From 434684c1e48aefcafd8ea884f847be9570ccf62c Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 15 Dec 2023 12:55:50 +0000 Subject: [PATCH 01/35] chore: incentive manager init --- Cargo.lock | 20 ++++ Cargo.toml | 1 + .../incentive-manager/.cargo/config | 4 + .../incentive-manager/.editorconfig | 11 ++ .../incentive-manager/.gitignore | 16 +++ .../incentive-manager/Cargo.toml | 41 +++++++ .../liquidity_hub/incentive-manager/README.md | 4 + .../incentive-manager/src/bin/schema.rs | 11 ++ .../incentive-manager/src/contract.rs | 41 +++++++ .../incentive-manager/src/error.rs | 13 +++ .../incentive-manager/src/helpers.rs | 27 +++++ .../incentive-manager/src/lib.rs | 6 ++ .../incentive-manager/src/state.rs | 1 + .../liquidity_hub/vault-manager/README.md | 101 +----------------- packages/white-whale/src/incentive_manager.rs | 57 ++++++++++ packages/white-whale/src/lib.rs | 1 + 16 files changed, 257 insertions(+), 98 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/.cargo/config create mode 100644 contracts/liquidity_hub/incentive-manager/.editorconfig create mode 100644 contracts/liquidity_hub/incentive-manager/.gitignore create mode 100644 contracts/liquidity_hub/incentive-manager/Cargo.toml create mode 100644 contracts/liquidity_hub/incentive-manager/README.md create mode 100644 contracts/liquidity_hub/incentive-manager/src/bin/schema.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/contract.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/error.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/helpers.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/lib.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/state.rs create mode 100644 packages/white-whale/src/incentive_manager.rs diff --git a/Cargo.lock b/Cargo.lock index 581171ffc..5e8647417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -861,6 +861,26 @@ dependencies = [ "white-whale", ] +[[package]] +name = "incentive-manager" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.20.0", + "cw-ownable", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw20", + "cw20-base", + "schemars", + "semver", + "serde", + "thiserror", + "white-whale", +] + [[package]] name = "indoc" version = "1.0.9" diff --git a/Cargo.toml b/Cargo.toml index 9a810fd40..d10c6f6fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "contracts/liquidity_hub/vault-network/*", "contracts/liquidity_hub/epoch-manager", "contracts/liquidity_hub/vault-manager", + "contracts/liquidity_hub/incentive-manager", ] [workspace.package] diff --git a/contracts/liquidity_hub/incentive-manager/.cargo/config b/contracts/liquidity_hub/incentive-manager/.cargo/config new file mode 100644 index 000000000..af5698e58 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/liquidity_hub/incentive-manager/.editorconfig b/contracts/liquidity_hub/incentive-manager/.editorconfig new file mode 100644 index 000000000..3d36f20b1 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/contracts/liquidity_hub/incentive-manager/.gitignore b/contracts/liquidity_hub/incentive-manager/.gitignore new file mode 100644 index 000000000..9095deaa4 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/.gitignore @@ -0,0 +1,16 @@ +# Build results +/target +/schema + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/liquidity_hub/incentive-manager/Cargo.toml b/contracts/liquidity_hub/incentive-manager/Cargo.toml new file mode 100644 index 000000000..1c57159ed --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "incentive-manager" +version = "0.1.0" +authors = ["Kerber0x "] +edition.workspace = true +description = "The Incentive Manager is a contract that allows to manage multiple incentives in a single contract." +license.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +publish.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +injective = ["white-whale/injective"] +token_factory = ["white-whale/token_factory"] +osmosis_token_factory = ["white-whale/osmosis_token_factory"] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-schema.workspace = true +cosmwasm-std.workspace = true +cw-storage-plus.workspace = true +cw2.workspace = true +cw20.workspace = true +cw20-base.workspace = true +schemars.workspace = true +semver.workspace = true +serde.workspace = true +thiserror.workspace = true +white-whale.workspace = true +cw-utils.workspace = true +cw-ownable.workspace = true + +[dev-dependencies] +cw-multi-test.workspace = true diff --git a/contracts/liquidity_hub/incentive-manager/README.md b/contracts/liquidity_hub/incentive-manager/README.md new file mode 100644 index 000000000..6cada5e4c --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/README.md @@ -0,0 +1,4 @@ +# Incentive Manager + +The Incentive Manager is the V2 iteration of the original incentives. This is a monolithic contract that handles all +the incentives-related logic. \ No newline at end of file diff --git a/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs b/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs new file mode 100644 index 000000000..869b35542 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use incentive_manager::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs new file mode 100644 index 000000000..35e172435 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -0,0 +1,41 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +// use cw2::set_contract_version; + +use crate::error::ContractError; +use white_whale::incentive_manager::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +/* +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:incentive-manager"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +*/ + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + unimplemented!() +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsg, +) -> Result { + unimplemented!() +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { + unimplemented!() +} + +#[cfg(test)] +mod tests {} diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs new file mode 100644 index 000000000..4a69d8ff2 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -0,0 +1,13 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. +} diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs new file mode 100644 index 000000000..6ffc885df --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -0,0 +1,27 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, StdResult, WasmMsg}; + +use crate::msg::ExecuteMsg; + +/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers +/// for working with this. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct CwTemplateContract(pub Addr); + +impl CwTemplateContract { + pub fn addr(&self) -> Addr { + self.0.clone() + } + + pub fn call>(&self, msg: T) -> StdResult { + let msg = to_json_binary(&msg.into())?; + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg, + funds: vec![], + } + .into()) + } +} diff --git a/contracts/liquidity_hub/incentive-manager/src/lib.rs b/contracts/liquidity_hub/incentive-manager/src/lib.rs new file mode 100644 index 000000000..596ab8b0c --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/lib.rs @@ -0,0 +1,6 @@ +pub mod contract; +mod error; +pub mod helpers; +pub mod state; + +pub use crate::error::ContractError; diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -0,0 +1 @@ + diff --git a/contracts/liquidity_hub/vault-manager/README.md b/contracts/liquidity_hub/vault-manager/README.md index 054ea4814..6460cda1f 100644 --- a/contracts/liquidity_hub/vault-manager/README.md +++ b/contracts/liquidity_hub/vault-manager/README.md @@ -1,99 +1,4 @@ -# CosmWasm Starter Pack +# Vault Manager -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. - -## Creating a new repo from template - -Assuming you have a recent version of Rust and Cargo installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: - -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: - -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` - -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: - -**Latest** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -``` - -For cloning minimal code repo: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -d minimal=true -``` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -``` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. +The Vault Manager is the V2 iteration of the original WW vault network. This is a monolithic contract that handles all +the vaults and flashloans in the White Whale DEX. \ No newline at end of file diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs new file mode 100644 index 000000000..e444f6d7c --- /dev/null +++ b/packages/white-whale/src/incentive_manager.rs @@ -0,0 +1,57 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; +use crate::pool_network::asset::{Asset, AssetInfo}; +use crate::vault_manager::LpTokenType; + +/// The instantiation message +#[cw_serde] +pub struct InstantiateMsg { + /// The owner of the contract + pub owner: String, + /// The whale lair address, where protocol fees are distributed + pub whale_lair_addr: String, + /// The fee that must be paid to create a flow. + pub create_flow_fee: Asset, + /// The maximum amount of flows that can exist for a single LP token at a time. + pub max_concurrent_flows: u64, + /// New flows are allowed to start up to `current_epoch + start_epoch_buffer` into the future. + pub max_flow_epoch_buffer: u64, + /// The minimum amount of time that a user can bond their tokens for. In nanoseconds. + pub min_unbonding_duration: u64, + /// The maximum amount of time that a user can bond their tokens for. In nanoseconds. + pub max_unbonding_duration: u64, +} + +/// The execution messages +#[cw_ownable_execute] +#[cw_serde] +pub enum ExecuteMsg { + /// Creates a new incentive contract tied to the `lp_asset` specified. + CreateIncentive { params: IncentiveParams }, +} + +/// The migrate message +#[cw_serde] +pub struct MigrateMsg {} + +/// The query messages +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Retrieves the configuration of the manager. + #[returns(Config)] + Config {}, +} + + +/// Configuration for the contract (manager) +#[cw_serde] + +pub struct Config {} + +#[cw_serde] +pub struct IncentiveParams { + lp_asset: AssetInfo, + +} diff --git a/packages/white-whale/src/lib.rs b/packages/white-whale/src/lib.rs index 1b97f22f0..a4857ebaa 100644 --- a/packages/white-whale/src/lib.rs +++ b/packages/white-whale/src/lib.rs @@ -13,3 +13,4 @@ pub mod traits; pub mod vault_manager; pub mod vault_network; pub mod whale_lair; +pub mod incentive_manager; From 8dab7d64ac8525f393c0e682302c4bd55a240f6e Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 27 Dec 2023 19:44:00 +0000 Subject: [PATCH 02/35] chore: initial commit incentive manager --- .../incentive-manager/src/bin/schema.rs | 2 +- .../incentive-manager/src/contract.rs | 125 +++++++++--- .../incentive-manager/src/error.rs | 90 ++++++++- .../incentive-manager/src/helpers.rs | 178 ++++++++++++++++-- .../incentive-manager/src/lib.rs | 1 + .../incentive-manager/src/manager/commands.rs | 131 +++++++++++++ .../incentive-manager/src/manager/mod.rs | 1 + .../incentive-manager/src/state.rs | 123 ++++++++++++ .../liquidity_hub/vault-manager/src/state.rs | 4 +- packages/white-whale/src/incentive_manager.rs | 92 +++++++-- packages/white-whale/src/lib.rs | 2 +- .../white-whale/src/pool_network/asset.rs | 20 ++ 12 files changed, 712 insertions(+), 57 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/src/manager/commands.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/manager/mod.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs b/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs index 869b35542..8e5fecb16 100644 --- a/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs +++ b/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::write_api; -use incentive_manager::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use white_whale::incentive_manager::{ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { write_api! { diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 35e172435..32a0a3a3a 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -1,41 +1,124 @@ -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; -// use cw2::set_contract_version; +use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw2::{get_contract_version, set_contract_version}; +use semver::Version; + +use white_whale::incentive_manager::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; +use white_whale::vault_manager::MigrateMsg; use crate::error::ContractError; -use white_whale::incentive_manager::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::manager; +use crate::state::CONFIG; -/* -// version info for migration info const CONTRACT_NAME: &str = "crates.io:incentive-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -*/ -#[cfg_attr(not(feature = "library"), entry_point)] +#[entry_point] pub fn instantiate( - _deps: DepsMut, + deps: DepsMut, _env: Env, _info: MessageInfo, - _msg: InstantiateMsg, + msg: InstantiateMsg, ) -> Result { - unimplemented!() + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // ensure that max_concurrent_incentives is non-zero + if msg.max_concurrent_incentives == 0 { + return Err(ContractError::UnspecifiedConcurrentIncentives); + } + + if msg.max_unbonding_duration < msg.min_unbonding_duration { + return Err(ContractError::InvalidUnbondingRange { + min: msg.min_unbonding_duration, + max: msg.max_unbonding_duration, + }); + } + + let config = Config { + epoch_manager_addr: deps.api.addr_validate(&msg.epoch_manager_addr)?, + whale_lair_addr: deps.api.addr_validate(&msg.whale_lair_addr)?, + create_incentive_fee: msg.create_incentive_fee, + max_concurrent_incentives: msg.max_concurrent_incentives, + max_incentive_epoch_buffer: msg.max_incentive_epoch_buffer, + min_unbonding_duration: msg.min_unbonding_duration, + max_unbonding_duration: msg.max_unbonding_duration, + }; + + CONFIG.save(deps.storage, &config)?; + + cw_ownable::initialize_owner(deps.storage, deps.api, Some(msg.owner.as_str()))?; + + Ok(Response::default().add_attributes(vec![ + ("action", "instantiate".to_string()), + ("owner", msg.owner.to_string()), + ("epoch_manager_addr", config.epoch_manager_addr.to_string()), + ("whale_lair_addr", config.whale_lair_addr.to_string()), + ("create_flow_fee", config.create_incentive_fee.to_string()), + ( + "max_concurrent_flows", + config.max_concurrent_incentives.to_string(), + ), + ( + "max_flow_epoch_buffer", + config.max_incentive_epoch_buffer.to_string(), + ), + ( + "min_unbonding_duration", + config.min_unbonding_duration.to_string(), + ), + ( + "max_unbonding_duration", + config.max_unbonding_duration.to_string(), + ), + ])) } -#[cfg_attr(not(feature = "library"), entry_point)] +#[entry_point] pub fn execute( - _deps: DepsMut, - _env: Env, - _info: MessageInfo, - _msg: ExecuteMsg, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, ) -> Result { - unimplemented!() + match msg { + ExecuteMsg::CreateIncentive { params } => { + manager::commands::create_incentive(deps, env, info, params) + } + ExecuteMsg::UpdateOwnership(action) => { + Ok( + cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map( + |ownership| { + Response::default() + .add_attribute("action", "update_ownership") + .add_attributes(ownership.into_attributes()) + }, + )?, + ) + } + } } -#[cfg_attr(not(feature = "library"), entry_point)] +#[entry_point] pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { unimplemented!() } -#[cfg(test)] -mod tests {} +#[cfg(not(tarpaulin_include))] +#[entry_point] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + use white_whale::migrate_guards::check_contract_name; + + 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()?; + + if storage_version >= version { + return Err(ContractError::MigrateInvalidVersion { + current_version: storage_version, + new_version: version, + }); + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 4a69d8ff2..7571752df 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{OverflowError, StdError, Uint128}; +use cw_ownable::OwnershipError; +use semver::Version; use thiserror::Error; #[derive(Error, Debug)] @@ -6,8 +8,90 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("Semver parsing error: {0}")] + SemVer(String), + #[error("Unauthorized")] Unauthorized {}, - // Add any other custom errors you like here. - // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. + + #[error("{0}")] + OwnershipError(#[from] OwnershipError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("max_concurrent_flows cannot be set to zero")] + UnspecifiedConcurrentIncentives, + + #[error("Invalid unbonding range, specified min as {min} and max as {max}")] + InvalidUnbondingRange { + /// The minimum unbonding time + min: u64, + /// The maximum unbonding time + max: u64, + }, + + #[error("Incentive doesn't exist")] + NonExistentIncentive {}, + + #[error("Attempt to create a new incentive, which exceeds the maximum of {max} incentives allowed per LP at a time")] + TooManyIncentives { + /// The maximum amount of incentives that can exist + max: u32, + }, + + #[error("Attempt to create a new incentive with a small incentive_asset amount, which is less than the minimum of {min}")] + InvalidIncentiveAmount { + /// The minimum amount of an asset to create an incentive with + min: u128, + }, + + #[error("The asset sent is not supported for fee payments")] + FeeAssetNotSupported, + + #[error("Incentive creation fee was not included")] + IncentiveFeeMissing, + + #[error("The incentive you are intending to create doesn't meet the minimum required of {min} after taking the fee")] + EmptyIncentiveAfterFee { min: u128 }, + + #[error("The asset sent doesn't match the asset expected")] + AssetMismatch, + + #[error( + "Incentive creation fee was not fulfilled, only {paid_amount} / {required_amount} present" + )] + IncentiveFeeNotPaid { + /// The amount that was paid + paid_amount: Uint128, + /// The amount that needed to be paid + required_amount: Uint128, + }, + + #[error("Specified incentive asset was not transferred")] + IncentiveAssetNotSent, + + #[error("The end epoch for this incentive is invalid")] + InvalidEndEpoch {}, + + #[error("Incentive end timestamp was set to a time in the past")] + IncentiveEndsInPast, + + #[error("Incentive start timestamp is after the end timestamp")] + IncentiveStartTimeAfterEndTime, + + #[error("Incentive start timestamp is too far into the future")] + IncentiveStartTooFar, + + #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] + MigrateInvalidVersion { + new_version: Version, + current_version: Version, + }, +} + +impl From for ContractError { + fn from(err: semver::Error) -> Self { + Self::SemVer(err.to_string()) + } } diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 6ffc885df..91a85a53d 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -1,27 +1,169 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; -use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, StdResult, WasmMsg}; +use cosmwasm_std::{wasm_execute, BankMsg, Coin, CosmosMsg, Deps, Env, MessageInfo}; -use crate::msg::ExecuteMsg; +use white_whale::incentive_manager::{Config, IncentiveParams}; +use white_whale::pool_network::asset::{Asset, AssetInfo}; -/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers -/// for working with this. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct CwTemplateContract(pub Addr); +use crate::ContractError; -impl CwTemplateContract { - pub fn addr(&self) -> Addr { - self.0.clone() +/// Processes the incentive creation fee and returns the appropriate messages to be sent +pub(crate) fn process_incentive_creation_fee( + config: &Config, + info: &MessageInfo, + incentive_creation_fee: &Asset, + params: &mut IncentiveParams, +) -> Result, ContractError> { + let mut messages: Vec = vec![]; + + // verify the fee to create an incentive is being paid + match incentive_creation_fee.info.clone() { + AssetInfo::Token { .. } => { + // only fees in native tokens are supported + return Err(ContractError::FeeAssetNotSupported); + } + AssetInfo::NativeToken { + denom: incentive_creation_fee_denom, + } => { + // check paid fee amount + let paid_fee_amount = info + .funds + .iter() + .find(|coin| coin.denom == incentive_creation_fee_denom) + .ok_or(ContractError::IncentiveFeeMissing)? + .amount; + + match paid_fee_amount.cmp(&incentive_creation_fee.amount) { + Ordering::Equal => (), // do nothing if user paid correct amount, + Ordering::Less => { + // user underpaid + return Err(ContractError::IncentiveFeeNotPaid { + paid_amount: paid_fee_amount, + required_amount: incentive_creation_fee.amount, + }); + } + Ordering::Greater => { + // if the user is paying more than the incentive_creation_fee, check if it's trying to create + // and incentive with the same asset as the incentive_creation_fee. + // otherwise, refund the difference + match params.incentive_asset.info.clone() { + AssetInfo::Token { .. } => {} + AssetInfo::NativeToken { + denom: incentive_asset_denom, + } => { + if incentive_creation_fee_denom == incentive_asset_denom { + // check if the amounts add up, i.e. the fee + incentive asset = paid amount. That is because the incentive asset + // and the creation fee asset are the same, all go in the info.funds of the transaction + if params + .incentive_asset + .amount + .checked_add(incentive_creation_fee.amount)? + != paid_fee_amount + { + return Err(ContractError::AssetMismatch); + } + } else { + messages.push( + BankMsg::Send { + to_address: info.sender.clone().into_string(), + amount: vec![Coin { + amount: paid_fee_amount - incentive_creation_fee.amount, + denom: incentive_creation_fee_denom.clone(), + }], + } + .into(), + ); + } + } + } + } + } + + // send incentive creation fee to whale lair for distribution + messages.push(white_whale::whale_lair::fill_rewards_msg( + config.whale_lair_addr.clone().into_string(), + vec![incentive_creation_fee.to_owned()], + )?); + } } - pub fn call>(&self, msg: T) -> StdResult { - let msg = to_json_binary(&msg.into())?; - Ok(WasmMsg::Execute { - contract_addr: self.addr().into(), - msg, - funds: vec![], + Ok(messages) +} + +/// Asserts the incentive asset was sent correctly, considering the incentive creation fee if applicable. +/// Returns a vector of messages to be sent (applies only when the incentive asset is a CW20 token) +pub(crate) fn assert_incentive_asset( + deps: Deps, + env: &Env, + info: &MessageInfo, + incentive_creation_fee: &Asset, + params: &mut IncentiveParams, +) -> Result, ContractError> { + let mut messages: Vec = vec![]; + + match params.incentive_asset.info.clone() { + AssetInfo::NativeToken { + denom: incentive_asset_denom, + } => { + let coin_sent = info + .funds + .iter() + .find(|sent| sent.denom == incentive_asset_denom) + .ok_or(ContractError::AssetMismatch)?; + + match incentive_creation_fee.info.clone() { + AssetInfo::Token { .. } => {} // only fees in native tokens are supported + AssetInfo::NativeToken { + denom: incentive_fee_denom, + } => { + if incentive_fee_denom != incentive_asset_denom { + if coin_sent.amount != params.incentive_asset.amount { + return Err(ContractError::AssetMismatch); + } + } else { + if params + .incentive_asset + .amount + .checked_add(incentive_creation_fee.amount)? + != coin_sent.amount + { + return Err(ContractError::AssetMismatch); + } + } + } + } + } + AssetInfo::Token { + contract_addr: incentive_asset_contract_addr, + } => { + // make sure the incentive asset has enough allowance + let allowance: cw20::AllowanceResponse = deps.querier.query_wasm_smart( + incentive_asset_contract_addr.clone(), + &cw20::Cw20QueryMsg::Allowance { + owner: info.sender.clone().into_string(), + spender: env.contract.address.clone().into_string(), + }, + )?; + + if allowance.allowance < params.incentive_asset.amount { + return Err(ContractError::AssetMismatch); + } + + // create the transfer message to the incentive manager + messages.push( + wasm_execute( + env.contract.address.clone().into_string(), + &cw20::Cw20ExecuteMsg::TransferFrom { + owner: info.sender.clone().into_string(), + recipient: env.contract.address.clone().into_string(), + amount: params.incentive_asset.amount, + }, + vec![], + )? + .into(), + ); } - .into()) } + + Ok(messages) } diff --git a/contracts/liquidity_hub/incentive-manager/src/lib.rs b/contracts/liquidity_hub/incentive-manager/src/lib.rs index 596ab8b0c..585ac494d 100644 --- a/contracts/liquidity_hub/incentive-manager/src/lib.rs +++ b/contracts/liquidity_hub/incentive-manager/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; mod error; pub mod helpers; +mod manager; pub mod state; pub use crate::error::ContractError; diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs new file mode 100644 index 000000000..76bc49d59 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -0,0 +1,131 @@ +use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Uint128}; +use std::collections::HashMap; + +use white_whale::epoch_manager::epoch_manager::EpochResponse; +use white_whale::incentive_manager::{Curve, Incentive, IncentiveParams}; + +use crate::helpers::{assert_incentive_asset, process_incentive_creation_fee}; +use crate::state::{get_incentives_by_lp_asset, CONFIG, INCENTIVE_COUNTER}; +use crate::ContractError; + +/// Minimum amount of an asset to create an incentive with +pub const MIN_INCENTIVE_AMOUNT: Uint128 = Uint128::new(1_000u128); + +/// Default incentive duration in epochs +pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; + +/// Creates an incentive with the given params +pub(crate) fn create_incentive( + deps: DepsMut, + env: Env, + info: MessageInfo, + mut params: IncentiveParams, +) -> Result { + // check if more incentives can be created for this particular LP asset + let config = CONFIG.load(deps.storage)?; + let incentives = get_incentives_by_lp_asset( + deps.storage, + ¶ms.lp_asset, + None, + Some(config.max_concurrent_incentives), + )?; + if incentives.len() == config.max_concurrent_incentives as usize { + return Err(ContractError::TooManyIncentives { + max: config.max_concurrent_incentives, + }); + } + + // check the flow is being created with a valid amount + if params.incentive_asset.amount < MIN_INCENTIVE_AMOUNT { + return Err(ContractError::InvalidIncentiveAmount { + min: MIN_INCENTIVE_AMOUNT.u128(), + }); + } + + let mut messages: Vec = vec![]; + + let incentive_creation_fee = config.create_incentive_fee.clone(); + + if incentive_creation_fee.amount != Uint128::zero() { + // verify the fee to create an incentive is being paid + messages.append(&mut process_incentive_creation_fee( + &config, + &info, + &incentive_creation_fee, + &mut params, + )?); + } + + // verify the incentive asset was sent + messages.append(&mut assert_incentive_asset( + deps.as_ref(), + &env, + &info, + &incentive_creation_fee, + &mut params, + )?); + + // assert epoch params are correctly set + + let epoch_response: EpochResponse = deps.querier.query_wasm_smart( + config.epoch_manager_addr.into_string(), + &white_whale::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + + let current_epoch = epoch_response.epoch.id; + + let end_epoch = params.end_epoch.unwrap_or( + current_epoch + .checked_add(DEFAULT_INCENTIVE_DURATION) + .ok_or(ContractError::InvalidEndEpoch {})?, + ); + + // ensure the incentive is set to end in a future epoch + if current_epoch > end_epoch { + return Err(ContractError::IncentiveEndsInPast); + } + + let start_epoch = params.start_epoch.unwrap_or(current_epoch); + + // ensure that start date is before end date + if start_epoch > end_epoch { + return Err(ContractError::IncentiveStartTimeAfterEndTime); + } + + // ensure that start date is set within buffer + if start_epoch > current_epoch + u64::from(config.max_incentive_epoch_buffer) { + return Err(ContractError::IncentiveStartTooFar); + } + + // create incentive identifier + let incentive_id = INCENTIVE_COUNTER + .update::<_, StdError>(deps.storage, |current_id| Ok(current_id + 1u64))?; + let incentive_identifier = params + .incentive_indentifier + .unwrap_or(incentive_id.to_string()); + + // create the incentive + let incentive = Incentive { + incentive_identifier, + start_epoch, + end_epoch, + emitted_tokens: HashMap::new(), + curve: params.curve.unwrap_or(Curve::Linear), + incentive_asset: params.incentive_asset, + lp_asset: params.lp_asset, + incentive_creator: info.sender, + claimed_amount: Uint128::zero(), + asset_history: Default::default(), + }; + + Ok(Response::default().add_attributes(vec![ + ("action", "create_incentive".to_string()), + ("incentive_creator", incentive.incentive_creator.to_string()), + ("incentive_identifier", incentive.incentive_identifier), + ("start_epoch", incentive.start_epoch.to_string()), + ("end_epoch", incentive.end_epoch.to_string()), + ("curve", incentive.curve.to_string()), + ("incentive_asset", incentive.incentive_asset.to_string()), + ("lp_asset", incentive.lp_asset.to_string()), + ])) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs b/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs new file mode 100644 index 000000000..82b6da3c0 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs @@ -0,0 +1 @@ +pub mod commands; diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 8b1378917..39c68dc34 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -1 +1,124 @@ +use cosmwasm_std::{Deps, Order, StdResult, Storage}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, MultiIndex}; +use white_whale::incentive_manager::{Config, Incentive}; +use white_whale::pool_network::asset::AssetInfo; + +use crate::ContractError; + +// Contract's config +pub const CONFIG: Item = Item::new("config"); + +/// An monotonically increasing counter to generate unique incentive identifiers. +pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); + +/// Incentives map +pub const INCENTIVES: IndexedMap = IndexedMap::new( + "incentives", + IncentiveIndexes { + lp_asset: MultiIndex::new( + |_pk, i| i.lp_asset.to_string(), + "incentives", + "incentives__lp_asset", + ), + incentive_asset: MultiIndex::new( + |_pk, i| i.incentive_asset.to_string(), + "incentives", + "incentives__incentive_asset", + ), + }, +); + +pub struct IncentiveIndexes<'a> { + pub lp_asset: MultiIndex<'a, String, Incentive, String>, + pub incentive_asset: MultiIndex<'a, String, Incentive, String>, +} + +impl<'a> IndexList for IncentiveIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.lp_asset, &self.incentive_asset]; + Box::new(v.into_iter()) + } +} + +// settings for pagination +pub(crate) const MAX_LIMIT: u32 = 100; +const DEFAULT_LIMIT: u32 = 10; + +/// Gets the incentives in the contract +pub fn get_incentives( + storage: &dyn Storage, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = cw_utils::calc_range_start_string(start_after).map(Bound::ExclusiveRaw); + + INCENTIVES + .range(storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, incentive) = item?; + + Ok(incentive) + }) + .collect() +} + +/// Gets incentives given an lp asset [AssetInfo] +pub fn get_incentives_by_lp_asset( + storage: &dyn Storage, + lp_asset: &AssetInfo, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = cw_utils::calc_range_start_string(start_after).map(Bound::ExclusiveRaw); + + INCENTIVES + .idx + .lp_asset + .prefix(lp_asset.to_string()) + .range(storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, incentive) = item?; + + Ok(incentive) + }) + .collect() +} + +/// Gets incentives given an incentive asset as [AssetInfo] +pub fn get_incentive_by_asset( + storage: &dyn Storage, + incentive_asset: &AssetInfo, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = cw_utils::calc_range_start_string(start_after).map(Bound::ExclusiveRaw); + + INCENTIVES + .idx + .incentive_asset + .prefix(incentive_asset.to_string()) + .range(storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, incentive) = item?; + + Ok(incentive) + }) + .collect() +} + +/// Gets the incentive given its identifier +pub fn get_incentive_by_identifier( + deps: &Deps, + incentive_identifier: String, +) -> Result { + INCENTIVES + .may_load(deps.storage, incentive_identifier)? + .ok_or(ContractError::NonExistentIncentive {}) +} diff --git a/contracts/liquidity_hub/vault-manager/src/state.rs b/contracts/liquidity_hub/vault-manager/src/state.rs index aaec2e056..e9d5006c6 100644 --- a/contracts/liquidity_hub/vault-manager/src/state.rs +++ b/contracts/liquidity_hub/vault-manager/src/state.rs @@ -71,8 +71,8 @@ pub fn get_vaults( /// Calculates the item at which to start the range fn calc_range_start(start_after: Option>) -> Option> { - start_after.map(|asset_info| { - let mut v = asset_info; + start_after.map(|item| { + let mut v = item; v.push(1); v }) diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index e444f6d7c..5466b9269 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -1,21 +1,25 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; +use std::collections::{BTreeMap, HashMap}; + use crate::pool_network::asset::{Asset, AssetInfo}; -use crate::vault_manager::LpTokenType; /// The instantiation message #[cw_serde] pub struct InstantiateMsg { /// The owner of the contract pub owner: String, + /// The epoch manager address, where the epochs are managed + pub epoch_manager_addr: String, /// The whale lair address, where protocol fees are distributed pub whale_lair_addr: String, - /// The fee that must be paid to create a flow. - pub create_flow_fee: Asset, - /// The maximum amount of flows that can exist for a single LP token at a time. - pub max_concurrent_flows: u64, - /// New flows are allowed to start up to `current_epoch + start_epoch_buffer` into the future. - pub max_flow_epoch_buffer: u64, + /// The fee that must be paid to create an incentive. + pub create_incentive_fee: Asset, + /// The maximum amount of incentives that can exist for a single LP token at a time. + pub max_concurrent_incentives: u32, + /// New incentives are allowed to start up to `current_epoch + start_epoch_buffer` into the future. + pub max_incentive_epoch_buffer: u32, /// The minimum amount of time that a user can bond their tokens for. In nanoseconds. pub min_unbonding_duration: u64, /// The maximum amount of time that a user can bond their tokens for. In nanoseconds. @@ -44,14 +48,80 @@ pub enum QueryMsg { Config {}, } - /// Configuration for the contract (manager) #[cw_serde] +pub struct Config { + /// The address to of the whale lair, to send fees to. + pub whale_lair_addr: Addr, + /// The epoch manager address, where the epochs are managed + pub epoch_manager_addr: Addr, + /// The fee that must be paid to create an incentive. + pub create_incentive_fee: Asset, + /// The maximum amount of incentives that can exist for a single LP token at a time. + pub max_concurrent_incentives: u32, + /// The maximum amount of epochs in the future a new incentive is allowed to start in. + pub max_incentive_epoch_buffer: u32, + /// The minimum amount of time that a user can bond their tokens for. In nanoseconds. + pub min_unbonding_duration: u64, + /// The maximum amount of time that a user can bond their tokens for. In nanoseconds. + pub max_unbonding_duration: u64, +} -pub struct Config {} - +/// Parameters for creating incentive #[cw_serde] pub struct IncentiveParams { - lp_asset: AssetInfo, + /// The LP asset to create the incentive for. + pub lp_asset: AssetInfo, + /// The epoch at which the incentive will start. If unspecified, it will start at the + /// current epoch. + pub start_epoch: Option, + /// The epoch at which the incentive should end. If unspecified, the incentive will default to end at + /// 14 epochs from the current one. + pub end_epoch: Option, + /// The type of distribution curve. If unspecified, the distribution will be linear. + pub curve: Option, + /// The asset to be distributed in this incentive. + pub incentive_asset: Asset, + /// If set, it will be used to identify the incentive. + pub incentive_indentifier: Option, +} + +/// Represents an incentive. +#[cw_serde] +pub struct Incentive { + /// The ID of the incentive. + pub incentive_identifier: String, + /// The account which opened the incentive and can manage it. + pub incentive_creator: Addr, + /// The LP asset to create the incentive for. + pub lp_asset: AssetInfo, + /// The asset the incentive was created to distribute. + pub incentive_asset: Asset, + /// The amount of the `incentive_asset` that has been claimed so far. + pub claimed_amount: Uint128, + /// The type of curve the incentive has. + pub curve: Curve, + /// The epoch at which the incentive starts. + pub start_epoch: u64, + /// The epoch at which the incentive 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 incentive is expanded. + pub asset_history: BTreeMap, +} + +#[cw_serde] +pub enum Curve { + /// A linear curve that releases assets uniformly over time. + Linear, +} +impl std::fmt::Display for Curve { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Curve::Linear => write!(f, "linear"), + } + } } diff --git a/packages/white-whale/src/lib.rs b/packages/white-whale/src/lib.rs index a4857ebaa..783c89aa7 100644 --- a/packages/white-whale/src/lib.rs +++ b/packages/white-whale/src/lib.rs @@ -4,6 +4,7 @@ pub mod epoch_manager; pub mod fee; pub mod fee_collector; pub mod fee_distributor; +pub mod incentive_manager; pub mod lp_common; pub mod migrate_guards; pub mod pool_network; @@ -13,4 +14,3 @@ pub mod traits; pub mod vault_manager; pub mod vault_network; pub mod whale_lair; -pub mod incentive_manager; diff --git a/packages/white-whale/src/pool_network/asset.rs b/packages/white-whale/src/pool_network/asset.rs index 68fae153a..f6b4ffd17 100644 --- a/packages/white-whale/src/pool_network/asset.rs +++ b/packages/white-whale/src/pool_network/asset.rs @@ -132,6 +132,26 @@ impl Asset { AssetInfo::NativeToken { denom } => denom, } } + + /// Builds a native asset + pub fn native_asset>(denom: S, amount: Uint128) -> Asset { + Asset { + info: AssetInfo::NativeToken { + denom: denom.into(), + }, + amount, + } + } + + /// Builds a cw20 token asset + pub fn token_asset>(contract_addr: S, amount: Uint128) -> Asset { + Asset { + info: AssetInfo::Token { + contract_addr: contract_addr.into(), + }, + amount, + } + } } /// AssetInfo contract_addr is usually passed from the cw20 hook From 29b1badf852651c74f4c1b8c7ba9dbdf8deb96fb Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 28 Dec 2023 16:44:51 +0000 Subject: [PATCH 03/35] chore: update message to manage incentives --- .../incentive-manager/src/contract.rs | 16 ++++-- .../incentive-manager/src/error.rs | 3 ++ .../incentive-manager/src/manager/commands.rs | 49 +++++++++++++++++-- .../incentive-manager/src/state.rs | 8 +-- packages/white-whale/src/incentive_manager.rs | 17 +++++-- 5 files changed, 79 insertions(+), 14 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 32a0a3a3a..f109f5d75 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -2,7 +2,9 @@ use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Respons use cw2::{get_contract_version, set_contract_version}; use semver::Version; -use white_whale::incentive_manager::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; +use white_whale::incentive_manager::{ + Config, ExecuteMsg, IncentiveAction, InstantiateMsg, QueryMsg, +}; use white_whale::vault_manager::MigrateMsg; use crate::error::ContractError; @@ -80,9 +82,15 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::CreateIncentive { params } => { - manager::commands::create_incentive(deps, env, info, params) - } + ExecuteMsg::ManageIncentive { action } => match action { + IncentiveAction::Create { params } => { + manager::commands::create_incentive(deps, env, info, params) + } + IncentiveAction::Close { + incentive_identifier, + } => manager::commands::close_incentive(deps, info, incentive_identifier), + IncentiveAction::Extend { params } => Ok(Response::default()), + }, ExecuteMsg::UpdateOwnership(action) => { Ok( cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map( diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 7571752df..1a4e4f51c 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -20,6 +20,9 @@ pub enum ContractError { #[error("{0}")] OverflowError(#[from] OverflowError), + #[error("An incentive with the given identifier already exists")] + IncentiveAlreadyExists, + #[error("max_concurrent_flows cannot be set to zero")] UnspecifiedConcurrentIncentives, diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 76bc49d59..3f34624c4 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -1,11 +1,14 @@ -use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Uint128}; use std::collections::HashMap; +use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Uint128}; + use white_whale::epoch_manager::epoch_manager::EpochResponse; use white_whale::incentive_manager::{Curve, Incentive, IncentiveParams}; use crate::helpers::{assert_incentive_asset, process_incentive_creation_fee}; -use crate::state::{get_incentives_by_lp_asset, CONFIG, INCENTIVE_COUNTER}; +use crate::state::{ + get_incentive_by_identifier, get_incentives_by_lp_asset, CONFIG, INCENTIVES, INCENTIVE_COUNTER, +}; use crate::ContractError; /// Minimum amount of an asset to create an incentive with @@ -66,7 +69,6 @@ pub(crate) fn create_incentive( )?); // assert epoch params are correctly set - let epoch_response: EpochResponse = deps.querier.query_wasm_smart( config.epoch_manager_addr.into_string(), &white_whale::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, @@ -104,6 +106,12 @@ pub(crate) fn create_incentive( .incentive_indentifier .unwrap_or(incentive_id.to_string()); + // make sure another incentive with the same identifier doesn't exist + match get_incentive_by_identifier(deps.storage, &incentive_identifier) { + Ok(_) => return Err(ContractError::IncentiveAlreadyExists {}), + Err(_) => {} // the incentive does not exist, all good, continue + } + // create the incentive let incentive = Incentive { incentive_identifier, @@ -129,3 +137,38 @@ pub(crate) fn create_incentive( ("lp_asset", incentive.lp_asset.to_string()), ])) } + +/// Closes an incentive +pub(crate) fn close_incentive( + deps: DepsMut, + info: MessageInfo, + incentive_identifier: String, +) -> Result { + // validate that user is allowed to close the incentive. Only the incentive creator or the owner of the contract can close an incentive + let mut incentive = get_incentive_by_identifier(deps.storage, &incentive_identifier)?; + if !(incentive.incentive_creator == info.sender + || cw_ownable::is_owner(deps.storage, &info.sender)?) + { + return Err(ContractError::Unauthorized {}); + } + + // remove the incentive from the storage + INCENTIVES.remove(deps.storage, incentive_identifier.clone())?; + + // return the available asset, i.e. the amount that hasn't been claimed + incentive.incentive_asset.amount = incentive + .incentive_asset + .amount + .saturating_sub(incentive.claimed_amount); + + Ok(Response::default() + .add_message( + incentive + .incentive_asset + .into_msg(incentive.incentive_creator)?, + ) + .add_attributes(vec![ + ("action", "close_incentive".to_string()), + ("incentive_identifier", incentive_identifier), + ])) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 39c68dc34..a0b205eb1 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Deps, Order, StdResult, Storage}; +use cosmwasm_std::{Order, StdResult, Storage}; use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, MultiIndex}; use white_whale::incentive_manager::{Config, Incentive}; @@ -115,10 +115,10 @@ pub fn get_incentive_by_asset( /// Gets the incentive given its identifier pub fn get_incentive_by_identifier( - deps: &Deps, - incentive_identifier: String, + storage: &dyn Storage, + incentive_identifier: &String, ) -> Result { INCENTIVES - .may_load(deps.storage, incentive_identifier)? + .may_load(storage, incentive_identifier.clone())? .ok_or(ContractError::NonExistentIncentive {}) } diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index 5466b9269..515cc5421 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -1,7 +1,8 @@ +use std::collections::{BTreeMap, HashMap}; + use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; -use std::collections::{BTreeMap, HashMap}; use crate::pool_network::asset::{Asset, AssetInfo}; @@ -30,8 +31,11 @@ pub struct InstantiateMsg { #[cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { - /// Creates a new incentive contract tied to the `lp_asset` specified. - CreateIncentive { params: IncentiveParams }, + /// Manages an incentive based on the action, which can be: + /// - Create: Creates a new incentive. + /// - Close: Closes an existing incentive. + /// - Extend: Extends an existing incentive. + ManageIncentive { action: IncentiveAction }, } /// The migrate message @@ -86,6 +90,13 @@ pub struct IncentiveParams { pub incentive_indentifier: Option, } +#[cw_serde] +pub enum IncentiveAction { + Create { params: IncentiveParams }, + Close { incentive_identifier: String }, + Extend { params: IncentiveParams }, +} + /// Represents an incentive. #[cw_serde] pub struct Incentive { From 25878b0b39730f900c3f1c6ef2af4da61ddbe721 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 29 Dec 2023 13:09:46 +0000 Subject: [PATCH 04/35] chore(incentive_manager): implement closing incentives --- .../incentive-manager/src/contract.rs | 4 +- .../incentive-manager/src/helpers.rs | 35 ++++- .../incentive-manager/src/manager/commands.rs | 141 ++++++++++-------- .../incentive-manager/src/manager/mod.rs | 12 ++ .../white-whale/src/epoch_manager/common.rs | 12 ++ packages/white-whale/src/epoch_manager/mod.rs | 1 + packages/white-whale/src/incentive_manager.rs | 10 ++ 7 files changed, 154 insertions(+), 61 deletions(-) create mode 100644 packages/white-whale/src/epoch_manager/common.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index f109f5d75..e83ac1c59 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -89,7 +89,9 @@ pub fn execute( IncentiveAction::Close { incentive_identifier, } => manager::commands::close_incentive(deps, info, incentive_identifier), - IncentiveAction::Extend { params } => Ok(Response::default()), + IncentiveAction::Extend { params } => { + manager::commands::expand_incentive(deps, env, info, params) + } }, ExecuteMsg::UpdateOwnership(action) => { Ok( diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 91a85a53d..c808424e4 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use cosmwasm_std::{wasm_execute, BankMsg, Coin, CosmosMsg, Deps, Env, MessageInfo}; -use white_whale::incentive_manager::{Config, IncentiveParams}; +use white_whale::incentive_manager::{Config, IncentiveParams, DEFAULT_INCENTIVE_DURATION}; use white_whale::pool_network::asset::{Asset, AssetInfo}; use crate::ContractError; @@ -167,3 +167,36 @@ pub(crate) fn assert_incentive_asset( Ok(messages) } + +/// Asserts the incentive epochs are valid. Returns a tuple of (start_epoch, end_epoch) for the incentive +pub(crate) fn assert_incentive_epochs( + params: &IncentiveParams, + current_epoch: u64, + max_incentive_epoch_buffer: u64, +) -> Result<(u64, u64), ContractError> { + // assert epoch params are correctly set + let end_epoch = params.end_epoch.unwrap_or( + current_epoch + .checked_add(DEFAULT_INCENTIVE_DURATION) + .ok_or(ContractError::InvalidEndEpoch {})?, + ); + + // ensure the incentive is set to end in a future epoch + if current_epoch > end_epoch { + return Err(ContractError::IncentiveEndsInPast); + } + + let start_epoch = params.start_epoch.unwrap_or(current_epoch); + + // ensure that start date is before end date + if start_epoch > end_epoch { + return Err(ContractError::IncentiveStartTimeAfterEndTime); + } + + // ensure that start date is set within buffer + if start_epoch > current_epoch + max_incentive_epoch_buffer { + return Err(ContractError::IncentiveStartTooFar); + } + + Ok((start_epoch, end_epoch)) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 3f34624c4..bb0702a93 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -1,22 +1,20 @@ use std::collections::HashMap; -use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Uint128}; +use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Storage, Uint128}; -use white_whale::epoch_manager::epoch_manager::EpochResponse; -use white_whale::incentive_manager::{Curve, Incentive, IncentiveParams}; +use white_whale::incentive_manager::{ + Curve, Incentive, IncentiveParams, DEFAULT_INCENTIVE_DURATION, +}; -use crate::helpers::{assert_incentive_asset, process_incentive_creation_fee}; +use crate::helpers::{ + assert_incentive_asset, assert_incentive_epochs, process_incentive_creation_fee, +}; +use crate::manager::MIN_INCENTIVE_AMOUNT; use crate::state::{ get_incentive_by_identifier, get_incentives_by_lp_asset, CONFIG, INCENTIVES, INCENTIVE_COUNTER, }; use crate::ContractError; -/// Minimum amount of an asset to create an incentive with -pub const MIN_INCENTIVE_AMOUNT: Uint128 = Uint128::new(1_000u128); - -/// Default incentive duration in epochs -pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; - /// Creates an incentive with the given params pub(crate) fn create_incentive( deps: DepsMut, @@ -24,7 +22,7 @@ pub(crate) fn create_incentive( info: MessageInfo, mut params: IncentiveParams, ) -> Result { - // check if more incentives can be created for this particular LP asset + // check if there are any expired incentives for this LP asset let config = CONFIG.load(deps.storage)?; let incentives = get_incentives_by_lp_asset( deps.storage, @@ -32,21 +30,37 @@ pub(crate) fn create_incentive( None, Some(config.max_concurrent_incentives), )?; + + let current_epoch = white_whale::epoch_manager::common::get_current_epoch( + deps.as_ref(), + config.epoch_manager_addr.clone().into_string(), + )?; + + let (expired_incentives, incentives): (Vec<_>, Vec<_>) = incentives + .into_iter() + .partition(|incentive| incentive.is_expired(current_epoch)); + + let mut messages: Vec = vec![]; + + // close expired incentives if there are any + if !expired_incentives.is_empty() { + messages.append(&mut close_incentives(deps.storage, expired_incentives)?); + } + + // check if more incentives can be created for this particular LP asset if incentives.len() == config.max_concurrent_incentives as usize { return Err(ContractError::TooManyIncentives { max: config.max_concurrent_incentives, }); } - // check the flow is being created with a valid amount + // check the incentive is being created with a valid amount if params.incentive_asset.amount < MIN_INCENTIVE_AMOUNT { return Err(ContractError::InvalidIncentiveAmount { min: MIN_INCENTIVE_AMOUNT.u128(), }); } - let mut messages: Vec = vec![]; - let incentive_creation_fee = config.create_incentive_fee.clone(); if incentive_creation_fee.amount != Uint128::zero() { @@ -69,36 +83,12 @@ pub(crate) fn create_incentive( )?); // assert epoch params are correctly set - let epoch_response: EpochResponse = deps.querier.query_wasm_smart( - config.epoch_manager_addr.into_string(), - &white_whale::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + let (start_epoch, end_epoch) = assert_incentive_epochs( + ¶ms, + current_epoch, + u64::from(config.max_incentive_epoch_buffer), )?; - let current_epoch = epoch_response.epoch.id; - - let end_epoch = params.end_epoch.unwrap_or( - current_epoch - .checked_add(DEFAULT_INCENTIVE_DURATION) - .ok_or(ContractError::InvalidEndEpoch {})?, - ); - - // ensure the incentive is set to end in a future epoch - if current_epoch > end_epoch { - return Err(ContractError::IncentiveEndsInPast); - } - - let start_epoch = params.start_epoch.unwrap_or(current_epoch); - - // ensure that start date is before end date - if start_epoch > end_epoch { - return Err(ContractError::IncentiveStartTimeAfterEndTime); - } - - // ensure that start date is set within buffer - if start_epoch > current_epoch + u64::from(config.max_incentive_epoch_buffer) { - return Err(ContractError::IncentiveStartTooFar); - } - // create incentive identifier let incentive_id = INCENTIVE_COUNTER .update::<_, StdError>(deps.storage, |current_id| Ok(current_id + 1u64))?; @@ -138,37 +128,70 @@ pub(crate) fn create_incentive( ])) } -/// Closes an incentive +/// Closes an incentive. If the incentive has expired, anyone can close it. Otherwise, only the +/// incentive creator or the owner of the contract can close an incentive. pub(crate) fn close_incentive( deps: DepsMut, info: MessageInfo, incentive_identifier: String, ) -> Result { // validate that user is allowed to close the incentive. Only the incentive creator or the owner of the contract can close an incentive + let config = CONFIG.load(deps.storage)?; + let current_epoch = white_whale::epoch_manager::common::get_current_epoch( + deps.as_ref(), + config.epoch_manager_addr.into_string(), + )?; + let mut incentive = get_incentive_by_identifier(deps.storage, &incentive_identifier)?; - if !(incentive.incentive_creator == info.sender - || cw_ownable::is_owner(deps.storage, &info.sender)?) + + if !(!incentive.is_expired(current_epoch) + && (incentive.incentive_creator == info.sender + || cw_ownable::is_owner(deps.storage, &info.sender)?)) { return Err(ContractError::Unauthorized {}); } - // remove the incentive from the storage - INCENTIVES.remove(deps.storage, incentive_identifier.clone())?; - - // return the available asset, i.e. the amount that hasn't been claimed - incentive.incentive_asset.amount = incentive - .incentive_asset - .amount - .saturating_sub(incentive.claimed_amount); - Ok(Response::default() - .add_message( - incentive - .incentive_asset - .into_msg(incentive.incentive_creator)?, - ) + .add_messages(close_incentives(deps.storage, vec![incentive])?) .add_attributes(vec![ ("action", "close_incentive".to_string()), ("incentive_identifier", incentive_identifier), ])) } + +/// Closes a list of incentives. Does not validate the sender, do so before calling this function. +pub(crate) fn close_incentives( + storage: &mut dyn Storage, + incentives: Vec, +) -> Result, ContractError> { + let mut messages: Vec = vec![]; + + for mut incentive in incentives { + // remove the incentive from the storage + INCENTIVES.remove(storage, incentive.incentive_identifier.clone())?; + + // return the available asset, i.e. the amount that hasn't been claimed + incentive.incentive_asset.amount = incentive + .incentive_asset + .amount + .saturating_sub(incentive.claimed_amount); + + messages.push( + incentive + .incentive_asset + .into_msg(incentive.incentive_creator)?, + ); + } + + Ok(messages) +} + +/// Expands an incentive with the given params +pub(crate) fn expand_incentive( + deps: DepsMut, + env: Env, + info: MessageInfo, + params: IncentiveParams, +) -> Result { + Ok(Response::default()) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs b/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs index 82b6da3c0..f67251f77 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs @@ -1 +1,13 @@ +use cosmwasm_std::Uint128; + pub mod commands; + +/// Minimum amount of an asset to create an incentive with +pub(crate) const MIN_INCENTIVE_AMOUNT: Uint128 = Uint128::new(1_000u128); +// If the end_epoch is not specified, the incentive will be expanded by DEFAULT_INCENTIVE_DURATION when +// the current epoch is within INCENTIVE_EXPANSION_BUFFER epochs from the end_epoch. +pub(crate) const INCENTIVE_EXPANSION_BUFFER: u64 = 5u64; +// An incentive can only be expanded for a maximum of INCENTIVE_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. +pub(crate) const INCENTIVE_EXPANSION_LIMIT: u64 = 180u64; diff --git a/packages/white-whale/src/epoch_manager/common.rs b/packages/white-whale/src/epoch_manager/common.rs new file mode 100644 index 000000000..e875bb5d6 --- /dev/null +++ b/packages/white-whale/src/epoch_manager/common.rs @@ -0,0 +1,12 @@ +use cosmwasm_std::{Deps, StdResult}; + +use crate::epoch_manager::epoch_manager::{EpochResponse, QueryMsg}; + +/// Queries the current epoch from the epoch manager contract +pub fn get_current_epoch(deps: Deps, epoch_manager_addr: String) -> StdResult { + let epoch_response: EpochResponse = deps + .querier + .query_wasm_smart(epoch_manager_addr, &QueryMsg::CurrentEpoch {})?; + + Ok(epoch_response.epoch.id) +} diff --git a/packages/white-whale/src/epoch_manager/mod.rs b/packages/white-whale/src/epoch_manager/mod.rs index 66d975625..39c5d2ecf 100644 --- a/packages/white-whale/src/epoch_manager/mod.rs +++ b/packages/white-whale/src/epoch_manager/mod.rs @@ -1,2 +1,3 @@ +pub mod common; pub mod epoch_manager; pub mod hooks; diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index 515cc5421..533a70f51 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -123,6 +123,13 @@ pub struct Incentive { pub asset_history: BTreeMap, } +impl Incentive { + /// Returns true if the incentive is expired at the given epoch. + pub fn is_expired(&self, epoch: u64) -> bool { + epoch > self.end_epoch + DEFAULT_INCENTIVE_DURATION + } +} + #[cw_serde] pub enum Curve { /// A linear curve that releases assets uniformly over time. @@ -136,3 +143,6 @@ impl std::fmt::Display for Curve { } } } + +/// Default incentive duration in epochs +pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; From ff85e9fb7549869bc0bab5801fb8dcce05ea0fdd Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 29 Dec 2023 19:36:27 +0000 Subject: [PATCH 05/35] chore(incentive_manager): add epoch changed hook --- Cargo.lock | 18 ++++++------ .../incentive-manager/src/contract.rs | 3 ++ .../incentive-manager/src/manager/commands.rs | 28 ++++++++++++++++--- packages/white-whale/Cargo.toml | 2 +- packages/white-whale/src/incentive_manager.rs | 3 ++ 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e8647417..415e033bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1103,9 +1103,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] @@ -1197,7 +1197,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.43", ] [[package]] @@ -1304,9 +1304,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.29" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1526,7 +1526,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.43", ] [[package]] @@ -1675,9 +1675,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.32" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -1791,7 +1791,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.43", ] [[package]] diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index e83ac1c59..4f80b5316 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -104,6 +104,9 @@ pub fn execute( )?, ) } + ExecuteMsg::EpochChangedHook(msg) => { + manager::commands::on_epoch_changed(deps, env, info, msg) + } } } diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index bb0702a93..66fef5387 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -2,18 +2,19 @@ use std::collections::HashMap; use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Storage, Uint128}; +use white_whale::epoch_manager::hooks::EpochChangedHookMsg; use white_whale::incentive_manager::{ - Curve, Incentive, IncentiveParams, DEFAULT_INCENTIVE_DURATION, + Curve, Incentive, IncentiveParams, }; +use crate::ContractError; use crate::helpers::{ assert_incentive_asset, assert_incentive_epochs, process_incentive_creation_fee, }; use crate::manager::MIN_INCENTIVE_AMOUNT; use crate::state::{ - get_incentive_by_identifier, get_incentives_by_lp_asset, CONFIG, INCENTIVES, INCENTIVE_COUNTER, + CONFIG, get_incentive_by_identifier, get_incentives_by_lp_asset, INCENTIVE_COUNTER, INCENTIVES, }; -use crate::ContractError; /// Creates an incentive with the given params pub(crate) fn create_incentive( @@ -146,7 +147,7 @@ pub(crate) fn close_incentive( if !(!incentive.is_expired(current_epoch) && (incentive.incentive_creator == info.sender - || cw_ownable::is_owner(deps.storage, &info.sender)?)) + || cw_ownable::is_owner(deps.storage, &info.sender)?)) { return Err(ContractError::Unauthorized {}); } @@ -195,3 +196,22 @@ pub(crate) fn expand_incentive( ) -> Result { Ok(Response::default()) } + +/// EpochChanged hook implementation + +pub(crate) fn on_epoch_changed( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: EpochChangedHookMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // only the epoch manager can trigger this + if info.sender != config.epoch_manager_addr { + return Err(ContractError::Unauthorized {}); + } + + + Ok(Response::default()) +} diff --git a/packages/white-whale/Cargo.toml b/packages/white-whale/Cargo.toml index c80da2912..8d19dece8 100644 --- a/packages/white-whale/Cargo.toml +++ b/packages/white-whale/Cargo.toml @@ -34,4 +34,4 @@ prost.workspace = true prost-types.workspace = true cw-ownable.workspace = true anybuf.workspace = true -cw-controllers.workspace = true \ No newline at end of file +cw-controllers.workspace = true diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index 533a70f51..2669c6b0a 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, HashMap}; +use crate::epoch_manager::hooks::EpochChangedHookMsg; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; @@ -36,6 +37,8 @@ pub enum ExecuteMsg { /// - Close: Closes an existing incentive. /// - Extend: Extends an existing incentive. ManageIncentive { action: IncentiveAction }, + /// Gets triggered by the epoch manager when a new epoch is created + EpochChangedHook(EpochChangedHookMsg), } /// The migrate message From 0a31aeab73beabab6cd846ccbefc910231b111f0 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 2 Jan 2024 17:11:37 +0000 Subject: [PATCH 06/35] chore: add message and structs for managing positions --- .../incentive-manager/src/contract.rs | 20 +++-- .../incentive-manager/src/error.rs | 3 + .../src/incentive/commands.rs | 24 ++++++ .../incentive-manager/src/incentive/mod.rs | 1 + .../incentive-manager/src/lib.rs | 2 + .../incentive-manager/src/manager/commands.rs | 73 +++++++++++------ .../src/position/commands.rs | 23 ++++++ .../incentive-manager/src/position/mod.rs | 1 + .../incentive-manager/src/state.rs | 17 +++- packages/white-whale/src/incentive_manager.rs | 82 ++++++++++++++++--- 10 files changed, 199 insertions(+), 47 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/position/commands.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/position/mod.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 4f80b5316..2dafc6569 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -3,13 +3,13 @@ use cw2::{get_contract_version, set_contract_version}; use semver::Version; use white_whale::incentive_manager::{ - Config, ExecuteMsg, IncentiveAction, InstantiateMsg, QueryMsg, + Config, ExecuteMsg, IncentiveAction, InstantiateMsg, PositionAction, QueryMsg, }; use white_whale::vault_manager::MigrateMsg; use crate::error::ContractError; -use crate::manager; use crate::state::CONFIG; +use crate::{incentive, manager, position}; const CONTRACT_NAME: &str = "crates.io:incentive-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -83,15 +83,12 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::ManageIncentive { action } => match action { - IncentiveAction::Create { params } => { - manager::commands::create_incentive(deps, env, info, params) + IncentiveAction::Fill { params } => { + manager::commands::fill_incentive(deps, env, info, params) } IncentiveAction::Close { incentive_identifier, } => manager::commands::close_incentive(deps, info, incentive_identifier), - IncentiveAction::Extend { params } => { - manager::commands::expand_incentive(deps, env, info, params) - } }, ExecuteMsg::UpdateOwnership(action) => { Ok( @@ -107,6 +104,15 @@ pub fn execute( ExecuteMsg::EpochChangedHook(msg) => { manager::commands::on_epoch_changed(deps, env, info, msg) } + ExecuteMsg::Claim() => incentive::commands::claim(deps, env, info), + ExecuteMsg::ManagePosition { action } => match action { + PositionAction::Fill { params } => { + position::commands::fill_position(deps, env, info, params) + } + PositionAction::Close { unbonding_duration } => { + position::commands::close_position(deps, env, info, unbonding_duration) + } + }, } } diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 1a4e4f51c..eda5ccdfc 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -91,6 +91,9 @@ pub enum ContractError { new_version: Version, current_version: Version, }, + + #[error("The sender doesn't have open positions to qualify for incentive rewards")] + NoOpenPositions, } impl From for ContractError { diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs new file mode 100644 index 000000000..33a5dce0a --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -0,0 +1,24 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use crate::state::{CONFIG, OPEN_POSITIONS}; +use crate::ContractError; + +/// Claims pending rewards for incentives where the user has LP +pub(crate) fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + // check if the user has any open LP positions + let mut open_positions = OPEN_POSITIONS + .may_load(deps.storage, &info.sender.clone())? + .unwrap_or(vec![]); + + if open_positions.is_empty() { + return Err(ContractError::NoOpenPositions {}); + } + + let config = CONFIG.load(deps.storage)?; + let current_epoch = white_whale::epoch_manager::common::get_current_epoch( + deps.as_ref(), + config.epoch_manager_addr.clone().into_string(), + )?; + + Ok(Response::default().add_attributes(vec![("action", "claim".to_string())])) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs new file mode 100644 index 000000000..82b6da3c0 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs @@ -0,0 +1 @@ +pub mod commands; diff --git a/contracts/liquidity_hub/incentive-manager/src/lib.rs b/contracts/liquidity_hub/incentive-manager/src/lib.rs index 585ac494d..3f4c88cc1 100644 --- a/contracts/liquidity_hub/incentive-manager/src/lib.rs +++ b/contracts/liquidity_hub/incentive-manager/src/lib.rs @@ -1,7 +1,9 @@ pub mod contract; mod error; pub mod helpers; +pub mod incentive; mod manager; +pub mod position; pub mod state; pub use crate::error::ContractError; diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 66fef5387..d7f143fe5 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -3,21 +3,41 @@ use std::collections::HashMap; use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Storage, Uint128}; use white_whale::epoch_manager::hooks::EpochChangedHookMsg; -use white_whale::incentive_manager::{ - Curve, Incentive, IncentiveParams, -}; +use white_whale::incentive_manager::{Curve, Incentive, IncentiveParams}; -use crate::ContractError; use crate::helpers::{ assert_incentive_asset, assert_incentive_epochs, process_incentive_creation_fee, }; use crate::manager::MIN_INCENTIVE_AMOUNT; use crate::state::{ - CONFIG, get_incentive_by_identifier, get_incentives_by_lp_asset, INCENTIVE_COUNTER, INCENTIVES, + get_incentive_by_identifier, get_incentives_by_lp_asset, CONFIG, INCENTIVES, INCENTIVE_COUNTER, }; +use crate::ContractError; + +pub(crate) fn fill_incentive( + deps: DepsMut, + env: Env, + info: MessageInfo, + params: IncentiveParams, +) -> Result { + // if an incentive_identifier was passed in the params, check if an incentive with such identifier + // exists and if the sender is allow to refill it, otherwise create a new incentive + if let Some(incentive_indentifier) = params.clone().incentive_indentifier { + let incentive_result = get_incentive_by_identifier(deps.storage, &incentive_indentifier); + match incentive_result { + // the incentive exists, try to expand it + Ok(incentive) => return expand_incentive(deps, env, info, incentive, params), + // the incentive does not exist, try to create it + Err(_) => {} + } + } + + // if no identifier was passed in the params or if the incentive does not exist, try to create the incentive + create_incentive(deps, env, info, params) +} /// Creates an incentive with the given params -pub(crate) fn create_incentive( +fn create_incentive( deps: DepsMut, env: Env, info: MessageInfo, @@ -105,22 +125,22 @@ pub(crate) fn create_incentive( // create the incentive let incentive = Incentive { - incentive_identifier, + identifier: incentive_identifier, start_epoch, end_epoch, - emitted_tokens: HashMap::new(), + //emitted_tokens: HashMap::new(), curve: params.curve.unwrap_or(Curve::Linear), incentive_asset: params.incentive_asset, lp_asset: params.lp_asset, - incentive_creator: info.sender, + owner: info.sender, claimed_amount: Uint128::zero(), - asset_history: Default::default(), + expansion_history: Default::default(), }; Ok(Response::default().add_attributes(vec![ ("action", "create_incentive".to_string()), - ("incentive_creator", incentive.incentive_creator.to_string()), - ("incentive_identifier", incentive.incentive_identifier), + ("incentive_creator", incentive.owner.to_string()), + ("incentive_identifier", incentive.identifier), ("start_epoch", incentive.start_epoch.to_string()), ("end_epoch", incentive.end_epoch.to_string()), ("curve", incentive.curve.to_string()), @@ -146,8 +166,7 @@ pub(crate) fn close_incentive( let mut incentive = get_incentive_by_identifier(deps.storage, &incentive_identifier)?; if !(!incentive.is_expired(current_epoch) - && (incentive.incentive_creator == info.sender - || cw_ownable::is_owner(deps.storage, &info.sender)?)) + && (incentive.owner == info.sender || cw_ownable::is_owner(deps.storage, &info.sender)?)) { return Err(ContractError::Unauthorized {}); } @@ -161,7 +180,7 @@ pub(crate) fn close_incentive( } /// Closes a list of incentives. Does not validate the sender, do so before calling this function. -pub(crate) fn close_incentives( +fn close_incentives( storage: &mut dyn Storage, incentives: Vec, ) -> Result, ContractError> { @@ -169,7 +188,7 @@ pub(crate) fn close_incentives( for mut incentive in incentives { // remove the incentive from the storage - INCENTIVES.remove(storage, incentive.incentive_identifier.clone())?; + INCENTIVES.remove(storage, incentive.identifier.clone())?; // return the available asset, i.e. the amount that hasn't been claimed incentive.incentive_asset.amount = incentive @@ -177,24 +196,31 @@ pub(crate) fn close_incentives( .amount .saturating_sub(incentive.claimed_amount); - messages.push( - incentive - .incentive_asset - .into_msg(incentive.incentive_creator)?, - ); + messages.push(incentive.incentive_asset.into_msg(incentive.owner)?); } Ok(messages) } /// Expands an incentive with the given params -pub(crate) fn expand_incentive( +fn expand_incentive( deps: DepsMut, env: Env, info: MessageInfo, + incentive: Incentive, params: IncentiveParams, ) -> Result { - Ok(Response::default()) + // only the incentive owner can expand it + if incentive.owner != info.sender { + return Err(ContractError::Unauthorized {}); + } + + // validate the params are correct and the incentive can actually be expanded + + Ok(Response::default().add_attributes(vec![ + ("action", "close_incentive".to_string()), + ("incentive_identifier", incentive.identifier), + ])) } /// EpochChanged hook implementation @@ -212,6 +238,5 @@ pub(crate) fn on_epoch_changed( return Err(ContractError::Unauthorized {}); } - Ok(Response::default()) } diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs new file mode 100644 index 000000000..08037e9cf --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use white_whale::incentive_manager::PositionParams; + +use crate::ContractError; + +pub(crate) fn fill_position( + deps: DepsMut, + env: Env, + info: MessageInfo, + params: PositionParams, +) -> Result { + Ok(Response::default().add_attributes(vec![("action", "fill_position".to_string())])) +} + +pub(crate) fn close_position( + deps: DepsMut, + env: Env, + info: MessageInfo, + unbonding_duration: u64, +) -> Result { + Ok(Response::default().add_attributes(vec![("action", "close_position".to_string())])) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs new file mode 100644 index 000000000..82b6da3c0 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs @@ -0,0 +1 @@ +pub mod commands; diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index a0b205eb1..472936eaa 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -1,7 +1,7 @@ -use cosmwasm_std::{Order, StdResult, Storage}; -use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, MultiIndex}; +use cosmwasm_std::{Addr, Order, StdResult, Storage}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; -use white_whale::incentive_manager::{Config, Incentive}; +use white_whale::incentive_manager::{Config, EpochId, Incentive, Position}; use white_whale::pool_network::asset::AssetInfo; use crate::ContractError; @@ -9,6 +9,17 @@ use crate::ContractError; // Contract's config pub const CONFIG: Item = Item::new("config"); +/// All open positions that a user have. Open positions accumulate rewards, and a user can have +/// multiple open positions active at once. +pub const OPEN_POSITIONS: Map<&Addr, Vec> = Map::new("open_positions"); + +/// All closed positions that users have. Closed positions don't accumulate rewards, and the +/// underlying tokens are claimable after `unbonding_duration`. +pub const CLOSED_POSITIONS: Map<&Addr, Vec> = Map::new("closed_positions"); + +/// The last epoch an address claimed rewards +pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch"); + /// An monotonically increasing counter to generate unique incentive identifiers. pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index 2669c6b0a..bda7690c9 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -1,10 +1,10 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; -use crate::epoch_manager::hooks::EpochChangedHookMsg; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; +use crate::epoch_manager::hooks::EpochChangedHookMsg; use crate::pool_network::asset::{Asset, AssetInfo}; /// The instantiation message @@ -33,12 +33,17 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { /// Manages an incentive based on the action, which can be: - /// - Create: Creates a new incentive. + /// - Fill: Creates or expands an incentive. /// - Close: Closes an existing incentive. - /// - Extend: Extends an existing incentive. ManageIncentive { action: IncentiveAction }, + /// Manages a position based on the action, which can be: + /// - Fill: Creates or expands a position. + /// - Close: Closes an existing position. + ManagePosition { action: PositionAction }, /// Gets triggered by the epoch manager when a new epoch is created EpochChangedHook(EpochChangedHookMsg), + /// Gets triggered by the epoch manager when a new epoch is created + Claim(), } /// The migrate message @@ -95,18 +100,59 @@ pub struct IncentiveParams { #[cw_serde] pub enum IncentiveAction { - Create { params: IncentiveParams }, - Close { incentive_identifier: String }, - Extend { params: IncentiveParams }, + /// Fills an incentive. If the incentive doesn't exist, it creates a new one. If it exists already, + /// it expands it given the sender created the original incentive and the params are correct. + Fill { + /// The parameters for the incentive to fill. + params: IncentiveParams + }, + //// Closes an incentive with the given identifier. If the incentive has expired, anyone can + // close it. Otherwise, only the incentive creator or the owner of the contract can close an incentive. + Close { + /// The incentive identifier to close. + incentive_identifier: String + }, } +#[cw_serde] +pub enum PositionAction { + /// Fills a position. If the position doesn't exist, it opens it. If it exists already, + /// it expands it given the sender opened the original position and the params are correct. + Fill { + /// The parameters for the position to fill. + params: PositionParams + }, + /// Closes an existing position. The position stops earning incentive rewards. + Close { + /// The unbonding duration of the position to close. + unbonding_duration: u64 + }, +} + + +/// Parameters for creating incentive +#[cw_serde] +pub struct PositionParams { + /// The amount to add to the position. + amount: Uint128, + /// The unbond completion timestamp to identify the position to add to. In nanoseconds. + unbonding_duration: u64, + /// The receiver for the position. + /// If left empty, defaults to the message sender. + receiver: Option, +} + + +// type for the epoch id +pub type EpochId = u64; + /// Represents an incentive. #[cw_serde] pub struct Incentive { /// The ID of the incentive. - pub incentive_identifier: String, + pub identifier: String, /// The account which opened the incentive and can manage it. - pub incentive_creator: Addr, + pub owner: Addr, /// The LP asset to create the incentive for. pub lp_asset: AssetInfo, /// The asset the incentive was created to distribute. @@ -116,14 +162,14 @@ pub struct Incentive { /// The type of curve the incentive has. pub curve: Curve, /// The epoch at which the incentive starts. - pub start_epoch: u64, + pub start_epoch: EpochId, /// The epoch at which the incentive ends. - pub end_epoch: u64, + pub end_epoch: EpochId, /// emitted tokens - pub emitted_tokens: HashMap, + //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 incentive is expanded. - pub asset_history: BTreeMap, + pub expansion_history: BTreeMap, } impl Incentive { @@ -149,3 +195,13 @@ impl std::fmt::Display for Curve { /// Default incentive duration in epochs pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; + + +/// Represents an LP position. +#[cw_serde] +pub struct Position { + /// The amount of LP tokens that are put up to earn incentives. + pub amount: Uint128, + /// Represents the amount of time in seconds the user must wait after unbonding for the LP tokens to be released. + pub unbonding_duration: u64, +} From 501e2d6e6a0e338f09c34bf698064bcbbcff3cc1 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 3 Jan 2024 17:14:24 +0000 Subject: [PATCH 07/35] chore: impl fill position --- .../incentive-manager/src/contract.rs | 2 + .../incentive-manager/src/error.rs | 49 ++++++- .../incentive-manager/src/helpers.rs | 28 +++- .../src/incentive/commands.rs | 2 + .../incentive-manager/src/manager/commands.rs | 10 +- .../src/position/commands.rs | 128 +++++++++++++++++- .../incentive-manager/src/position/helpers.rs | 119 ++++++++++++++++ .../incentive-manager/src/position/mod.rs | 1 + .../incentive-manager/src/state.rs | 12 +- packages/white-whale/src/incentive_manager.rs | 42 +++--- .../white-whale/src/pool_network/asset.rs | 7 + 11 files changed, 372 insertions(+), 28 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/src/position/helpers.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 2dafc6569..b811493e4 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -91,6 +91,8 @@ pub fn execute( } => manager::commands::close_incentive(deps, info, incentive_identifier), }, ExecuteMsg::UpdateOwnership(action) => { + cw_utils::nonpayable(&info)?; + Ok( cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map( |ownership| { diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index eda5ccdfc..0d62d7b26 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -1,5 +1,9 @@ -use cosmwasm_std::{OverflowError, StdError, Uint128}; +use cosmwasm_std::{ + CheckedFromRatioError, ConversionOverflowError, DivideByZeroError, OverflowError, StdError, + Uint128, +}; use cw_ownable::OwnershipError; +use cw_utils::PaymentError; use semver::Version; use thiserror::Error; @@ -14,12 +18,24 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, + #[error("{0}")] + PaymentError(#[from] PaymentError), + #[error("{0}")] OwnershipError(#[from] OwnershipError), #[error("{0}")] OverflowError(#[from] OverflowError), + #[error("{0}")] + CheckedFromRatioError(#[from] CheckedFromRatioError), + + #[error("{0}")] + ConversionOverflowError(#[from] ConversionOverflowError), + + #[error("{0}")] + DivideByZeroError(#[from] DivideByZeroError), + #[error("An incentive with the given identifier already exists")] IncentiveAlreadyExists, @@ -94,6 +110,37 @@ pub enum ContractError { #[error("The sender doesn't have open positions to qualify for incentive rewards")] NoOpenPositions, + + #[error( + "Invalid unbonding duration of {specified} specified, must be between {min} and {max}" + )] + InvalidUnbondingDuration { + /// The minimum amount of seconds that a user must bond for. + min: u64, + /// The maximum amount of seconds that a user can bond for. + max: u64, + /// The amount of seconds the user attempted to bond for. + specified: u64, + }, + + #[error("Attempt to create a position with {deposited_amount}, but only {allowance_amount} was set in allowance")] + MissingPositionDeposit { + /// The actual amount that the contract has an allowance for. + allowance_amount: Uint128, + /// The amount the account attempted to open a position with + deposited_amount: Uint128, + }, + + #[error("Attempt to create a position with {desired_amount}, but {paid_amount} was sent")] + MissingPositionDepositNative { + /// The amount the user intended to deposit. + desired_amount: Uint128, + /// The amount that was actually deposited. + paid_amount: Uint128, + }, + + #[error("Attempt to compute the weight of a duration of {unbonding_duration} which is outside the allowed bounds")] + InvalidWeight { unbonding_duration: u64 }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index c808424e4..5e4bd07b6 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -2,7 +2,9 @@ use std::cmp::Ordering; use cosmwasm_std::{wasm_execute, BankMsg, Coin, CosmosMsg, Deps, Env, MessageInfo}; -use white_whale::incentive_manager::{Config, IncentiveParams, DEFAULT_INCENTIVE_DURATION}; +use white_whale::incentive_manager::{ + Config, IncentiveParams, PositionParams, DEFAULT_INCENTIVE_DURATION, +}; use white_whale::pool_network::asset::{Asset, AssetInfo}; use crate::ContractError; @@ -168,8 +170,8 @@ pub(crate) fn assert_incentive_asset( Ok(messages) } -/// Asserts the incentive epochs are valid. Returns a tuple of (start_epoch, end_epoch) for the incentive -pub(crate) fn assert_incentive_epochs( +/// Validates the incentive epochs. Returns a tuple of (start_epoch, end_epoch) for the incentive. +pub(crate) fn validate_incentive_epochs( params: &IncentiveParams, current_epoch: u64, max_incentive_epoch_buffer: u64, @@ -200,3 +202,23 @@ pub(crate) fn assert_incentive_epochs( Ok((start_epoch, end_epoch)) } + +//todo maybe move this to position helpers?? +/// Validates the `unbonding_duration` specified in the position params is within the range specified +/// in the config. +pub(crate) fn validate_unbonding_duration( + config: &Config, + params: &PositionParams, +) -> Result<(), ContractError> { + if params.unbonding_duration < config.min_unbonding_duration + || params.unbonding_duration > config.max_unbonding_duration + { + return Err(ContractError::InvalidUnbondingDuration { + min: config.min_unbonding_duration, + max: config.max_unbonding_duration, + specified: params.unbonding_duration, + }); + } + + Ok(()) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index 33a5dce0a..db4651f32 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -5,6 +5,8 @@ use crate::ContractError; /// Claims pending rewards for incentives where the user has LP pub(crate) fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + cw_utils::nonpayable(&info)?; + // check if the user has any open LP positions let mut open_positions = OPEN_POSITIONS .may_load(deps.storage, &info.sender.clone())? diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index d7f143fe5..772cac046 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; - use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Storage, Uint128}; use white_whale::epoch_manager::hooks::EpochChangedHookMsg; use white_whale::incentive_manager::{Curve, Incentive, IncentiveParams}; use crate::helpers::{ - assert_incentive_asset, assert_incentive_epochs, process_incentive_creation_fee, + assert_incentive_asset, process_incentive_creation_fee, validate_incentive_epochs, }; use crate::manager::MIN_INCENTIVE_AMOUNT; use crate::state::{ @@ -104,7 +102,7 @@ fn create_incentive( )?); // assert epoch params are correctly set - let (start_epoch, end_epoch) = assert_incentive_epochs( + let (start_epoch, end_epoch) = validate_incentive_epochs( ¶ms, current_epoch, u64::from(config.max_incentive_epoch_buffer), @@ -156,6 +154,8 @@ pub(crate) fn close_incentive( info: MessageInfo, incentive_identifier: String, ) -> Result { + cw_utils::nonpayable(&info)?; + // validate that user is allowed to close the incentive. Only the incentive creator or the owner of the contract can close an incentive let config = CONFIG.load(deps.storage)?; let current_epoch = white_whale::epoch_manager::common::get_current_epoch( @@ -231,6 +231,8 @@ pub(crate) fn on_epoch_changed( info: MessageInfo, msg: EpochChangedHookMsg, ) -> Result { + cw_utils::nonpayable(&info)?; + let config = CONFIG.load(deps.storage)?; // only the epoch manager can trigger this diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 08037e9cf..118a24e39 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -1,7 +1,12 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError}; -use white_whale::incentive_manager::PositionParams; +use white_whale::incentive_manager::{Position, PositionParams}; +use crate::helpers::validate_unbonding_duration; +use crate::position::helpers::{calculate_weight, validate_funds_sent}; +use crate::state::{ + ADDRESS_LP_WEIGHT, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS, OPEN_POSITIONS, +}; use crate::ContractError; pub(crate) fn fill_position( @@ -10,9 +15,126 @@ pub(crate) fn fill_position( info: MessageInfo, params: PositionParams, ) -> Result { - Ok(Response::default().add_attributes(vec![("action", "fill_position".to_string())])) + // check if + let config = CONFIG.load(deps.storage)?; + + // validate unbonding duration + validate_unbonding_duration(&config, ¶ms)?; + + let mut messages: Vec = vec![]; + + // ensure the lp tokens are transferred to the contract. If the LP is a cw20 token, creates + // a transfer message + let transfer_token_msg = validate_funds_sent( + &deps.as_ref(), + env.clone(), + info.clone(), + params.clone().lp_asset, + )?; + + if let Some(transfer_token_msg) = transfer_token_msg { + messages.push(transfer_token_msg.into()); + } + + // if receiver was not specified, default to the sender of the message. + let receiver = params + .clone() + .receiver + .map(|r| deps.api.addr_validate(&r)) + .transpose()? + .map(|receiver| MessageInfo { + funds: info.funds.clone(), + sender: receiver, + }) + .unwrap_or_else(|| info.clone()); + + // check if there's an existing position with the given `unbonding_time` + let position_option = OPEN_POSITIONS + .may_load(deps.storage, &receiver.sender.clone())? + .unwrap_or_default() + .into_iter() + .find(|position| position.unbonding_duration == params.unbonding_duration); + + // if the position exist, expand it + if let Some(existing_position) = position_option { + expand_position(deps, &env, &receiver, ¶ms, &existing_position) + } else { + // otherwise, open it + open_position(deps, &env, &receiver, ¶ms) + } +} + +/// Expands an existing position +fn expand_position( + deps: DepsMut, + env: &Env, + receiver: &MessageInfo, + params: &PositionParams, + existing_position: &Position, +) -> Result { + Ok(Response::default().add_attributes(vec![("action", "expand_position".to_string())])) +} + +/// Opens a position +fn open_position( + deps: DepsMut, + env: &Env, + receiver: &MessageInfo, + params: &PositionParams, +) -> Result { + // add the position to the user's open positions + OPEN_POSITIONS.update::<_, StdError>(deps.storage, &receiver.sender, |positions| { + let mut positions = positions.unwrap_or_default(); + positions.push(Position { + lp_asset: params.clone().lp_asset, + unbonding_duration: params.unbonding_duration, + }); + + Ok(positions) + })?; + + // update the LP weight + let weight = calculate_weight(params)?; + LP_WEIGHTS.update::<_, StdError>( + deps.storage, + ¶ms.lp_asset.info.as_bytes(), + |lp_weight| Ok(lp_weight.unwrap_or_default().checked_add(weight)?), + )?; + + // update the user's weight for this LP + let mut address_lp_weight = ADDRESS_LP_WEIGHT + .may_load( + deps.storage, + (&receiver.sender, ¶ms.lp_asset.info.as_bytes()), + )? + .unwrap_or_default(); + address_lp_weight = address_lp_weight.checked_add(weight)?; + ADDRESS_LP_WEIGHT.save( + deps.storage, + (&receiver.sender, ¶ms.lp_asset.info.as_bytes()), + &address_lp_weight, + )?; + + let config = CONFIG.load(deps.storage)?; + let current_epoch = white_whale::epoch_manager::common::get_current_epoch( + deps.as_ref(), + config.epoch_manager_addr.clone().into_string(), + )?; + + ADDRESS_LP_WEIGHT_HISTORY.update::<_, StdError>( + deps.storage, + (&receiver.sender, current_epoch + 1u64), + |_| Ok(address_lp_weight), + )?; + + Ok(Response::default().add_attributes(vec![ + ("action", "open_position".to_string()), + ("receiver", receiver.sender.to_string()), + ("params", params.clone().to_string()), + ])) } +/// Closes an existing position pub(crate) fn close_position( deps: DepsMut, env: Env, diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs new file mode 100644 index 000000000..342ffa9aa --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -0,0 +1,119 @@ +use cosmwasm_std::{to_json_binary, Decimal256, Deps, Env, MessageInfo, Uint128, WasmMsg}; +use cw_utils::PaymentError; + +use white_whale::incentive_manager::PositionParams; +use white_whale::pool_network::asset::{Asset, AssetInfo}; + +use crate::ContractError; + +/// Validates that the message sender has sent the tokens to the contract. +/// In case the `lp_token` is a cw20 token, check if the sender set the specified `amount` as an +/// allowance for us to transfer for `lp_token`. +/// +/// If `lp_token` is a native token, check if the funds were sent in the [`MessageInfo`] struct. +/// +/// Returns the [`WasmMsg`] that will transfer the specified `amount` of the +/// `lp_token` to the contract. +pub fn validate_funds_sent( + deps: &Deps, + env: Env, + info: MessageInfo, + lp_asset: Asset, +) -> Result, ContractError> { + if lp_asset.amount.is_zero() { + return Err(ContractError::PaymentError(PaymentError::NoFunds {})); + } + + let send_lp_deposit_msg = match lp_asset.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 < lp_asset.amount { + return Err(ContractError::MissingPositionDeposit { + allowance_amount: allowance.allowance, + deposited_amount: lp_asset.amount, + }); + } + + // send the lp deposit to us + Some(WasmMsg::Execute { + contract_addr, + msg: to_json_binary(&cw20::Cw20ExecuteMsg::TransferFrom { + owner: info.sender.into_string(), + recipient: env.contract.address.into_string(), + amount: lp_asset.amount, + })?, + funds: vec![], + }) + } + AssetInfo::NativeToken { denom } => { + let paid_amount = cw_utils::must_pay(&info, &denom)?; + if paid_amount != lp_asset.amount { + return Err(ContractError::MissingPositionDepositNative { + desired_amount: lp_asset.amount, + paid_amount, + }); + } + // no message needed as native tokens are transferred together with the transaction + None + } + }; + + Ok(send_lp_deposit_msg) +} + +const SECONDS_IN_DAY: u64 = 86400; +const SECONDS_IN_YEAR: u64 = 31556926; + +/// Calculates the weight size for a user filling a position +pub fn calculate_weight(params: &PositionParams) -> Result { + if !(SECONDS_IN_DAY..=SECONDS_IN_YEAR).contains(¶ms.unbonding_duration) { + return Err(ContractError::InvalidWeight { + unbonding_duration: params.unbonding_duration, + }); + } + + // store in Uint128 form for later + let amount_uint = params.lp_asset.amount; + + // interpolate between [(86400, 1), (15778463, 5), (31556926, 16)] + // note that 31556926 is not exactly one 365-day year, but rather one Earth rotation year + // similarly, 15778463 is not 1/2 a 365-day year, but rather 1/2 a one Earth rotation year + + // first we need to convert into decimals + let unbonding_duration = Decimal256::from_atomics(params.unbonding_duration, 0).unwrap(); + let amount = Decimal256::from_atomics(params.lp_asset.amount, 0).unwrap(); + + let unbonding_duration_squared = unbonding_duration.checked_pow(2)?; + let unbonding_duration_mul = + unbonding_duration_squared.checked_mul(Decimal256::raw(109498841))?; + let unbonding_duration_part = + unbonding_duration_mul.checked_div(Decimal256::raw(7791996353100889432894))?; + + let next_part = unbonding_duration + .checked_mul(Decimal256::raw(249042009202369))? + .checked_div(Decimal256::raw(7791996353100889432894))?; + + let final_part = Decimal256::from_ratio(246210981355969u64, 246918738317569u64); + + let weight: Uint128 = amount + .checked_mul( + unbonding_duration_part + .checked_add(next_part)? + .checked_add(final_part)?, + )? + .atomics() + .checked_div(10u128.pow(18).into())? + .try_into()?; + + // we must clamp it to max(computed_value, amount) as + // otherwise we might get a multiplier of 0.999999999999999998 when + // computing the final_part decimal value, which is over 200 digits. + Ok(weight.max(amount_uint)) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs index 82b6da3c0..39da526a6 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs @@ -1 +1,2 @@ pub mod commands; +mod helpers; diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 472936eaa..70d5b45d8 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Order, StdResult, Storage}; +use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use white_whale::incentive_manager::{Config, EpochId, Incentive, Position}; @@ -20,6 +20,16 @@ pub const CLOSED_POSITIONS: Map<&Addr, Vec> = Map::new("closed_positio /// The last epoch an address claimed rewards pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch"); +/// The total weight (sum of all individual weights) of an LP asset +pub const LP_WEIGHTS: Map<&[u8], Uint128> = Map::new("lp_weights"); + +/// The weights for individual accounts +pub const ADDRESS_LP_WEIGHT: Map<(&Addr, &[u8]), Uint128> = Map::new("address_lp_weight"); + +/// The address lp weight history, i.e. how much lp weight an address had at a given epoch +pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, EpochId), Uint128> = + Map::new("address_lp_weight_history"); + /// An monotonically increasing counter to generate unique incentive identifiers. pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index bda7690c9..cd5ca3b21 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::fmt::Formatter; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128}; @@ -22,9 +23,9 @@ pub struct InstantiateMsg { pub max_concurrent_incentives: u32, /// New incentives are allowed to start up to `current_epoch + start_epoch_buffer` into the future. pub max_incentive_epoch_buffer: u32, - /// The minimum amount of time that a user can bond their tokens for. In nanoseconds. + /// The minimum amount of time that a user can bond their tokens for. In seconds. pub min_unbonding_duration: u64, - /// The maximum amount of time that a user can bond their tokens for. In nanoseconds. + /// The maximum amount of time that a user can bond their tokens for. In seconds. pub max_unbonding_duration: u64, } @@ -73,9 +74,9 @@ pub struct Config { pub max_concurrent_incentives: u32, /// The maximum amount of epochs in the future a new incentive is allowed to start in. pub max_incentive_epoch_buffer: u32, - /// The minimum amount of time that a user can bond their tokens for. In nanoseconds. + /// The minimum amount of time that a user can bond their tokens for. In seconds. pub min_unbonding_duration: u64, - /// The maximum amount of time that a user can bond their tokens for. In nanoseconds. + /// The maximum amount of time that a user can bond their tokens for. In seconds. pub max_unbonding_duration: u64, } @@ -104,13 +105,13 @@ pub enum IncentiveAction { /// it expands it given the sender created the original incentive and the params are correct. Fill { /// The parameters for the incentive to fill. - params: IncentiveParams + params: IncentiveParams, }, //// Closes an incentive with the given identifier. If the incentive has expired, anyone can // close it. Otherwise, only the incentive creator or the owner of the contract can close an incentive. Close { /// The incentive identifier to close. - incentive_identifier: String + incentive_identifier: String, }, } @@ -120,28 +121,38 @@ pub enum PositionAction { /// it expands it given the sender opened the original position and the params are correct. Fill { /// The parameters for the position to fill. - params: PositionParams + params: PositionParams, }, /// Closes an existing position. The position stops earning incentive rewards. Close { /// The unbonding duration of the position to close. - unbonding_duration: u64 + unbonding_duration: u64, }, } - /// Parameters for creating incentive #[cw_serde] pub struct PositionParams { - /// The amount to add to the position. - amount: Uint128, - /// The unbond completion timestamp to identify the position to add to. In nanoseconds. - unbonding_duration: u64, + /// The asset to add to the position. + pub lp_asset: Asset, + /// The unbond completion timestamp to identify the position to add to. In seconds. + pub unbonding_duration: u64, /// The receiver for the position. /// If left empty, defaults to the message sender. - receiver: Option, + pub receiver: Option, } +impl std::fmt::Display for PositionParams { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "lp_asset: {}, unbonding_duration: {}, receiver: {}", + self.lp_asset, + self.unbonding_duration, + self.receiver.as_ref().unwrap_or(&"".to_string()) + ) + } +} // type for the epoch id pub type EpochId = u64; @@ -196,12 +207,11 @@ impl std::fmt::Display for Curve { /// Default incentive duration in epochs pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; - /// Represents an LP position. #[cw_serde] pub struct Position { /// The amount of LP tokens that are put up to earn incentives. - pub amount: Uint128, + pub lp_asset: Asset, /// Represents the amount of time in seconds the user must wait after unbonding for the LP tokens to be released. pub unbonding_duration: u64, } diff --git a/packages/white-whale/src/pool_network/asset.rs b/packages/white-whale/src/pool_network/asset.rs index f6b4ffd17..592d0d697 100644 --- a/packages/white-whale/src/pool_network/asset.rs +++ b/packages/white-whale/src/pool_network/asset.rs @@ -184,6 +184,13 @@ impl AssetInfo { } } + pub fn as_bytes(&self) -> &[u8] { + match self { + AssetInfo::NativeToken { denom } => denom.as_bytes(), + AssetInfo::Token { contract_addr } => contract_addr.as_bytes(), + } + } + pub fn is_native_token(&self) -> bool { match self { AssetInfo::NativeToken { .. } => true, From 14e7ad00cde73b3d3f5987f1ed1e972cde77b938 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 4 Jan 2024 16:52:34 +0000 Subject: [PATCH 08/35] chore: impl expand position, refactor into fill_position fn --- .../src/position/commands.rs | 97 ++++++++++--------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 118a24e39..3a3a9cf07 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -15,7 +15,6 @@ pub(crate) fn fill_position( info: MessageInfo, params: PositionParams, ) -> Result { - // check if let config = CONFIG.load(deps.storage)?; // validate unbonding duration @@ -48,52 +47,64 @@ pub(crate) fn fill_position( }) .unwrap_or_else(|| info.clone()); - // check if there's an existing position with the given `unbonding_time` - let position_option = OPEN_POSITIONS - .may_load(deps.storage, &receiver.sender.clone())? + // check if there's an existing position with the given `unbonding_time`, get the index of it + // on the vector + let position_index = OPEN_POSITIONS + .may_load(deps.storage, &receiver.sender)? .unwrap_or_default() - .into_iter() - .find(|position| position.unbonding_duration == params.unbonding_duration); - - // if the position exist, expand it - if let Some(existing_position) = position_option { - expand_position(deps, &env, &receiver, ¶ms, &existing_position) - } else { - // otherwise, open it - open_position(deps, &env, &receiver, ¶ms) - } -} - -/// Expands an existing position -fn expand_position( - deps: DepsMut, - env: &Env, - receiver: &MessageInfo, - params: &PositionParams, - existing_position: &Position, -) -> Result { - Ok(Response::default().add_attributes(vec![("action", "expand_position".to_string())])) -} + .iter() + .enumerate() + .find(|(_, position)| position.unbonding_duration == params.unbonding_duration) + .map(|(index, _)| index); -/// Opens a position -fn open_position( - deps: DepsMut, - env: &Env, - receiver: &MessageInfo, - params: &PositionParams, -) -> Result { - // add the position to the user's open positions - OPEN_POSITIONS.update::<_, StdError>(deps.storage, &receiver.sender, |positions| { + // update the position + OPEN_POSITIONS.update::<_, ContractError>(deps.storage, &receiver.sender, |positions| { let mut positions = positions.unwrap_or_default(); - positions.push(Position { - lp_asset: params.clone().lp_asset, - unbonding_duration: params.unbonding_duration, - }); + + // if the position exists, expand it. Otherwise, create a new position by adding it to the vector + match position_index { + Some(index) => { + // Update the existing position at the given index + if let Some(pos) = positions.get_mut(index) { + if pos.lp_asset.info != params.lp_asset.info { + return Err(ContractError::AssetMismatch); + } + pos.lp_asset.amount = + pos.lp_asset.amount.checked_add(params.lp_asset.amount)?; + } + } + None => { + positions.push(Position { + lp_asset: params.clone().lp_asset, + unbonding_duration: params.unbonding_duration, + }); + } + } Ok(positions) })?; - // update the LP weight + // Update weights for the LP and the user + update_weights(deps, &receiver, ¶ms)?; + + let action = match position_index { + Some(_) => "expand_position", + None => "open_position", + }; + + Ok(Response::default().add_attributes(vec![ + ("action", action.to_string()), + ("receiver", receiver.sender.to_string()), + ("params", params.clone().to_string()), + ])) +} + +/// Updates the weights when managing a position +fn update_weights( + deps: DepsMut, + receiver: &MessageInfo, + params: &PositionParams, +) -> Result<(), ContractError> { let weight = calculate_weight(params)?; LP_WEIGHTS.update::<_, StdError>( deps.storage, @@ -127,11 +138,7 @@ fn open_position( |_| Ok(address_lp_weight), )?; - Ok(Response::default().add_attributes(vec![ - ("action", "open_position".to_string()), - ("receiver", receiver.sender.to_string()), - ("params", params.clone().to_string()), - ])) + Ok(()) } /// Closes an existing position From 005406c3df9ffb4468fef82ed6cee619cbf7e90f Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 4 Jan 2024 16:52:34 +0000 Subject: [PATCH 09/35] chore: impl expand position, refactor into fill_position fn --- scripts/deployment/deploy_env/base_migaloo.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deployment/deploy_env/base_migaloo.env b/scripts/deployment/deploy_env/base_migaloo.env index 39f265af8..3c5f64d09 100644 --- a/scripts/deployment/deploy_env/base_migaloo.env +++ b/scripts/deployment/deploy_env/base_migaloo.env @@ -1,6 +1,6 @@ if [ -n "$ZSH_VERSION" ]; then # Using an array for TXFLAG - TXFLAG=(--node $RPC --chain-id $CHAIN_ID --gas-prices 0.25$DENOM --gas auto --gas-adjustment 1.3 -y -b block --output json) + TXFLAG=(--node $RPC --chain-id $CHAIN_ID --gas-prices 1$DENOM --gas auto --gas-adjustment 1.3 -y -b block --output json) else # Using a string for TXFLAG TXFLAG="--node $RPC --chain-id $CHAIN_ID --gas-prices 0.25$DENOM --gas auto --gas-adjustment 1.3 -y -b block --output json" From b76b8960b7251dfebe3220c380dddbe1ef755013 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 24 Jan 2024 16:16:34 +0000 Subject: [PATCH 10/35] chore: abstracting the update_ownership function into white-whale common package --- Cargo.lock | 1 + .../liquidity_hub/epoch-manager/Cargo.toml | 1 + .../epoch-manager/src/commands.rs | 4 +++- .../epoch-manager/src/contract.rs | 4 ++-- .../liquidity_hub/epoch-manager/src/error.rs | 4 ++++ .../incentive-manager/src/contract.rs | 13 ++----------- .../vault-manager/src/contract.rs | 13 +++---------- .../vault-manager/src/manager/commands.rs | 1 + packages/white-whale/src/common.rs | 19 ++++++++++++++++++- 9 files changed, 35 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 415e033bd..5193cbf77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -590,6 +590,7 @@ dependencies = [ "cosmwasm-std", "cw-controllers", "cw-storage-plus", + "cw-utils", "cw2", "schemars", "semver", diff --git a/contracts/liquidity_hub/epoch-manager/Cargo.toml b/contracts/liquidity_hub/epoch-manager/Cargo.toml index b7a8608b4..8db2ee5a2 100644 --- a/contracts/liquidity_hub/epoch-manager/Cargo.toml +++ b/contracts/liquidity_hub/epoch-manager/Cargo.toml @@ -33,3 +33,4 @@ semver.workspace = true thiserror.workspace = true white-whale.workspace = true cw-controllers.workspace = true +cw-utils.workspace = true diff --git a/contracts/liquidity_hub/epoch-manager/src/commands.rs b/contracts/liquidity_hub/epoch-manager/src/commands.rs index f85c1de17..bcdbac8d8 100644 --- a/contracts/liquidity_hub/epoch-manager/src/commands.rs +++ b/contracts/liquidity_hub/epoch-manager/src/commands.rs @@ -27,7 +27,9 @@ pub(crate) fn remove_hook( } /// Creates a new epoch. -pub fn create_epoch(deps: DepsMut, env: Env) -> Result { +pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + cw_utils::nonpayable(&info)?; + let mut current_epoch = query_current_epoch(deps.as_ref())?.epoch; let config = CONFIG.load(deps.storage)?; diff --git a/contracts/liquidity_hub/epoch-manager/src/contract.rs b/contracts/liquidity_hub/epoch-manager/src/contract.rs index 0722b2444..4410b71fb 100644 --- a/contracts/liquidity_hub/epoch-manager/src/contract.rs +++ b/contracts/liquidity_hub/epoch-manager/src/contract.rs @@ -13,7 +13,7 @@ use crate::state::{ADMIN, CONFIG, EPOCHS}; use crate::{commands, queries}; // version info for migration info -const CONTRACT_NAME: &str = "white_whale-epoch-manager"; +const CONTRACT_NAME: &str = "white-whale_epoch-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[entry_point] @@ -69,7 +69,7 @@ pub fn execute( ExecuteMsg::RemoveHook { contract_addr } => { commands::remove_hook(deps, info, api, &contract_addr) } - ExecuteMsg::CreateEpoch {} => commands::create_epoch(deps, env), + ExecuteMsg::CreateEpoch {} => commands::create_epoch(deps, env, info), ExecuteMsg::UpdateConfig { owner, epoch_config, diff --git a/contracts/liquidity_hub/epoch-manager/src/error.rs b/contracts/liquidity_hub/epoch-manager/src/error.rs index d3eda4481..bb6e61aa3 100644 --- a/contracts/liquidity_hub/epoch-manager/src/error.rs +++ b/contracts/liquidity_hub/epoch-manager/src/error.rs @@ -1,5 +1,6 @@ use cosmwasm_std::StdError; use cw_controllers::{AdminError, HookError}; +use cw_utils::PaymentError; use semver::Version; use thiserror::Error; @@ -17,6 +18,9 @@ pub enum ContractError { #[error("The epoch id has overflowed.")] EpochOverflow, + #[error("{0}")] + PaymentError(#[from] PaymentError), + #[error("Semver parsing error: {0}")] SemVer(String), diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index b811493e4..2afa3c1df 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -11,7 +11,7 @@ use crate::error::ContractError; use crate::state::CONFIG; use crate::{incentive, manager, position}; -const CONTRACT_NAME: &str = "crates.io:incentive-manager"; +const CONTRACT_NAME: &str = "white-whale_incentive-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[entry_point] @@ -92,16 +92,7 @@ pub fn execute( }, ExecuteMsg::UpdateOwnership(action) => { cw_utils::nonpayable(&info)?; - - Ok( - cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map( - |ownership| { - Response::default() - .add_attribute("action", "update_ownership") - .add_attributes(ownership.into_attributes()) - }, - )?, - ) + white_whale::common::update_ownership(deps, env, info, action).map_err(Into::into) } ExecuteMsg::EpochChangedHook(msg) => { manager::commands::on_epoch_changed(deps, env, info, msg) diff --git a/contracts/liquidity_hub/vault-manager/src/contract.rs b/contracts/liquidity_hub/vault-manager/src/contract.rs index e642de838..9db4c2e82 100644 --- a/contracts/liquidity_hub/vault-manager/src/contract.rs +++ b/contracts/liquidity_hub/vault-manager/src/contract.rs @@ -15,7 +15,7 @@ use crate::state::{get_vault_by_lp, CONFIG, ONGOING_FLASHLOAN, VAULT_COUNTER}; use crate::{manager, queries, router, vault}; // version info for migration info -const CONTRACT_NAME: &str = "ww-vault-manager"; +const CONTRACT_NAME: &str = "white-whale_vault-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[entry_point] @@ -134,15 +134,8 @@ pub fn execute( } => router::commands::flash_loan(deps, env, info, asset, vault_identifier, payload), ExecuteMsg::Callback(msg) => router::commands::callback(deps, env, info, msg), ExecuteMsg::UpdateOwnership(action) => { - Ok( - cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map( - |ownership| { - Response::default() - .add_attribute("action", "update_ownership") - .add_attributes(ownership.into_attributes()) - }, - )?, - ) + cw_utils::nonpayable(&info)?; + white_whale::common::update_ownership(deps, env, info, action).map_err(Into::into) } } } diff --git a/contracts/liquidity_hub/vault-manager/src/manager/commands.rs b/contracts/liquidity_hub/vault-manager/src/manager/commands.rs index 1b851c220..a47b9c111 100644 --- a/contracts/liquidity_hub/vault-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/vault-manager/src/manager/commands.rs @@ -204,6 +204,7 @@ pub fn update_config( deposit_enabled: Option, withdraw_enabled: Option, ) -> Result { + cw_utils::nonpayable(&info)?; cw_ownable::assert_owner(deps.storage, &info.sender)?; let new_config = CONFIG.update::<_, ContractError>(deps.storage, |mut config| { diff --git a/packages/white-whale/src/common.rs b/packages/white-whale/src/common.rs index 6a9235762..fa1fbe90e 100644 --- a/packages/white-whale/src/common.rs +++ b/packages/white-whale/src/common.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{Addr, StdError, StdResult, Storage}; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage}; +use cw_ownable::{Action, OwnershipError}; use cw_storage_plus::Item; /// Validates that the given address matches the address stored in the given `owner_item`. @@ -16,3 +17,19 @@ pub fn validate_owner( Ok(()) } + +/// Updates the ownership of a contract using the cw_ownable package, which needs to be implemented by the contract. +pub fn update_ownership( + deps: DepsMut, + env: Env, + info: MessageInfo, + action: Action, +) -> Result { + Ok( + cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map(|ownership| { + Response::default() + .add_attribute("action", "update_ownership") + .add_attributes(ownership.into_attributes()) + })?, + ) +} From a8e3d86c8edb3798f6892f828dc1be0d756f680b Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 20 Feb 2024 16:03:19 +0000 Subject: [PATCH 11/35] chore: rework of the positions management --- .../incentive-manager/src/contract.rs | 48 ++- .../incentive-manager/src/error.rs | 22 +- .../incentive-manager/src/helpers.rs | 31 +- .../src/incentive/commands.rs | 2 +- .../incentive-manager/src/incentive/mod.rs | 1 + .../src/incentive/queries.rs | 17 + .../incentive-manager/src/manager/commands.rs | 21 +- .../src/position/commands.rs | 297 +++++++++++++----- .../incentive-manager/src/position/helpers.rs | 63 +++- .../incentive-manager/src/position/mod.rs | 1 + .../incentive-manager/src/position/queries.rs | 24 ++ .../incentive-manager/src/state.rs | 89 +++++- packages/white-whale/src/common.rs | 12 +- packages/white-whale/src/constants.rs | 1 + .../white-whale/src/epoch_manager/common.rs | 24 +- packages/white-whale/src/incentive_manager.rs | 85 +++-- 16 files changed, 564 insertions(+), 174 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/queries.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/position/queries.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 2afa3c1df..5764449ed 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -8,8 +8,10 @@ use white_whale::incentive_manager::{ use white_whale::vault_manager::MigrateMsg; use crate::error::ContractError; +use crate::helpers::validate_emergency_unlock_penalty; +use crate::position::commands::{close_position, fill_position, withdraw_position}; use crate::state::CONFIG; -use crate::{incentive, manager, position}; +use crate::{incentive, manager}; const CONTRACT_NAME: &str = "white-whale_incentive-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -28,10 +30,10 @@ pub fn instantiate( return Err(ContractError::UnspecifiedConcurrentIncentives); } - if msg.max_unbonding_duration < msg.min_unbonding_duration { + if msg.max_unlocking_duration < msg.min_unlocking_duration { return Err(ContractError::InvalidUnbondingRange { - min: msg.min_unbonding_duration, - max: msg.max_unbonding_duration, + min: msg.min_unlocking_duration, + max: msg.max_unlocking_duration, }); } @@ -41,8 +43,9 @@ pub fn instantiate( create_incentive_fee: msg.create_incentive_fee, max_concurrent_incentives: msg.max_concurrent_incentives, max_incentive_epoch_buffer: msg.max_incentive_epoch_buffer, - min_unbonding_duration: msg.min_unbonding_duration, - max_unbonding_duration: msg.max_unbonding_duration, + min_unlocking_duration: msg.min_unlocking_duration, + max_unlocking_duration: msg.max_unlocking_duration, + emergency_unlock_penalty: validate_emergency_unlock_penalty(msg.emergency_unlock_penalty)?, }; CONFIG.save(deps.storage, &config)?; @@ -65,11 +68,15 @@ pub fn instantiate( ), ( "min_unbonding_duration", - config.min_unbonding_duration.to_string(), + config.min_unlocking_duration.to_string(), ), ( "max_unbonding_duration", - config.max_unbonding_duration.to_string(), + config.max_unlocking_duration.to_string(), + ), + ( + "emergency_unlock_penalty", + config.emergency_unlock_penalty.to_string(), ), ])) } @@ -99,11 +106,26 @@ pub fn execute( } ExecuteMsg::Claim() => incentive::commands::claim(deps, env, info), ExecuteMsg::ManagePosition { action } => match action { - PositionAction::Fill { params } => { - position::commands::fill_position(deps, env, info, params) - } - PositionAction::Close { unbonding_duration } => { - position::commands::close_position(deps, env, info, unbonding_duration) + PositionAction::Fill { + identifier, + lp_asset, + unlocking_duration, + receiver, + } => fill_position( + deps, + env, + info, + identifier, + lp_asset, + unlocking_duration, + receiver, + ), + PositionAction::Close { + identifier, + lp_asset, + } => close_position(deps, env, info, identifier, lp_asset), + PositionAction::Withdraw { identifier } => { + withdraw_position(deps, env, info, identifier) } }, } diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 0d62d7b26..b4d1db043 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -108,13 +108,19 @@ pub enum ContractError { current_version: Version, }, - #[error("The sender doesn't have open positions to qualify for incentive rewards")] + #[error("The sender doesn't have open positions")] NoOpenPositions, + #[error("No position found with the given identifier: {identifier}")] + NoPositionFound { identifier: String }, + + #[error("The position has not expired yet")] + PositionNotExpired, + #[error( - "Invalid unbonding duration of {specified} specified, must be between {min} and {max}" + "Invalid unlocking duration of {specified} specified, must be between {min} and {max}" )] - InvalidUnbondingDuration { + InvalidUnlockingDuration { /// The minimum amount of seconds that a user must bond for. min: u64, /// The maximum amount of seconds that a user can bond for. @@ -139,8 +145,14 @@ pub enum ContractError { paid_amount: Uint128, }, - #[error("Attempt to compute the weight of a duration of {unbonding_duration} which is outside the allowed bounds")] - InvalidWeight { unbonding_duration: u64 }, + #[error("Attempt to compute the weight of a duration of {unlocking_duration} which is outside the allowed bounds")] + InvalidWeight { unlocking_duration: u64 }, + + #[error("The emergency unlock penalty provided is invalid")] + InvalidEmergencyUnlockPenalty, + + #[error("There're pending rewards to be claimed before this action can be executed")] + PendingRewards, } impl From for ContractError { diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 5e4bd07b6..e98703d92 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use cosmwasm_std::{wasm_execute, BankMsg, Coin, CosmosMsg, Deps, Env, MessageInfo}; +use cosmwasm_std::{wasm_execute, BankMsg, Coin, CosmosMsg, Decimal, Deps, Env, MessageInfo}; use white_whale::incentive_manager::{ Config, IncentiveParams, PositionParams, DEFAULT_INCENTIVE_DURATION, @@ -204,21 +204,32 @@ pub(crate) fn validate_incentive_epochs( } //todo maybe move this to position helpers?? -/// Validates the `unbonding_duration` specified in the position params is within the range specified +/// Validates the `unlocking_duration` specified in the position params is within the range specified /// in the config. -pub(crate) fn validate_unbonding_duration( +pub(crate) fn validate_unlocking_duration( config: &Config, - params: &PositionParams, + unlocking_duration: u64, ) -> Result<(), ContractError> { - if params.unbonding_duration < config.min_unbonding_duration - || params.unbonding_duration > config.max_unbonding_duration + if unlocking_duration < config.min_unlocking_duration + || unlocking_duration > config.max_unlocking_duration { - return Err(ContractError::InvalidUnbondingDuration { - min: config.min_unbonding_duration, - max: config.max_unbonding_duration, - specified: params.unbonding_duration, + return Err(ContractError::InvalidUnlockingDuration { + min: config.min_unlocking_duration, + max: config.max_unlocking_duration, + specified: unlocking_duration, }); } Ok(()) } + +/// Validates the emergency unlock penalty is within the allowed range (0-100%). Returns value it's validating, i.e. the penalty. +pub(crate) fn validate_emergency_unlock_penalty( + emergency_unlock_penalty: Decimal, +) -> Result { + if emergency_unlock_penalty > Decimal::percent(100) { + return Err(ContractError::InvalidEmergencyUnlockPenalty); + } + + Ok(emergency_unlock_penalty) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index db4651f32..41d79efa9 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -13,7 +13,7 @@ pub(crate) fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result, ContractError> { + Ok(INCENTIVES + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()? + .into_iter() + .filter(|((start_epoch, _), _)| start_epoch <= epoch) + .map(|(_, flow)| flow) + .collect::>()) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 772cac046..3a031d0c8 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -1,4 +1,5 @@ use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Storage, Uint128}; +use white_whale::epoch_manager::common::validate_epoch; use white_whale::epoch_manager::hooks::EpochChangedHookMsg; use white_whale::incentive_manager::{Curve, Incentive, IncentiveParams}; @@ -9,6 +10,7 @@ use crate::helpers::{ use crate::manager::MIN_INCENTIVE_AMOUNT; use crate::state::{ get_incentive_by_identifier, get_incentives_by_lp_asset, CONFIG, INCENTIVES, INCENTIVE_COUNTER, + LP_WEIGHTS_HISTORY, }; use crate::ContractError; @@ -20,7 +22,7 @@ pub(crate) fn fill_incentive( ) -> Result { // if an incentive_identifier was passed in the params, check if an incentive with such identifier // exists and if the sender is allow to refill it, otherwise create a new incentive - if let Some(incentive_indentifier) = params.clone().incentive_indentifier { + if let Some(incentive_indentifier) = params.clone().incentive_identifier { let incentive_result = get_incentive_by_identifier(deps.storage, &incentive_indentifier); match incentive_result { // the incentive exists, try to expand it @@ -54,10 +56,11 @@ fn create_incentive( deps.as_ref(), config.epoch_manager_addr.clone().into_string(), )?; + validate_epoch(¤t_epoch, env.block.time)?; let (expired_incentives, incentives): (Vec<_>, Vec<_>) = incentives .into_iter() - .partition(|incentive| incentive.is_expired(current_epoch)); + .partition(|incentive| incentive.is_expired(current_epoch.id)); let mut messages: Vec = vec![]; @@ -104,7 +107,7 @@ fn create_incentive( // assert epoch params are correctly set let (start_epoch, end_epoch) = validate_incentive_epochs( ¶ms, - current_epoch, + current_epoch.id, u64::from(config.max_incentive_epoch_buffer), )?; @@ -112,7 +115,7 @@ fn create_incentive( let incentive_id = INCENTIVE_COUNTER .update::<_, StdError>(deps.storage, |current_id| Ok(current_id + 1u64))?; let incentive_identifier = params - .incentive_indentifier + .incentive_identifier .unwrap_or(incentive_id.to_string()); // make sure another incentive with the same identifier doesn't exist @@ -165,7 +168,7 @@ pub(crate) fn close_incentive( let mut incentive = get_incentive_by_identifier(deps.storage, &incentive_identifier)?; - if !(!incentive.is_expired(current_epoch) + if !(!incentive.is_expired(current_epoch.id) && (incentive.owner == info.sender || cw_ownable::is_owner(deps.storage, &info.sender)?)) { return Err(ContractError::Unauthorized {}); @@ -195,7 +198,7 @@ fn close_incentives( .incentive_asset .amount .saturating_sub(incentive.claimed_amount); - + //TODO remake this into_msg since we are getting rid of the Asset struct in V2 messages.push(incentive.incentive_asset.into_msg(incentive.owner)?); } @@ -223,7 +226,7 @@ fn expand_incentive( ])) } -/// EpochChanged hook implementation +/// EpochChanged hook implementation. Updates the LP_WEIGHTS. pub(crate) fn on_epoch_changed( deps: DepsMut, @@ -239,6 +242,10 @@ pub(crate) fn on_epoch_changed( if info.sender != config.epoch_manager_addr { return Err(ContractError::Unauthorized {}); } + // + // LP_WEIGHTS_HISTORY. + // + // msg.current_epoch Ok(Response::default()) } diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 3a3a9cf07..3aa985449 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -1,44 +1,48 @@ use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError}; -use white_whale::incentive_manager::{Position, PositionParams}; +use white_whale::incentive_manager::Position; +use white_whale::pool_network::asset::Asset; -use crate::helpers::validate_unbonding_duration; -use crate::position::helpers::{calculate_weight, validate_funds_sent}; +use crate::helpers::validate_unlocking_duration; +use crate::position::helpers::{ + calculate_weight, get_latest_address_weight, get_latest_lp_weight, validate_funds_sent, +}; use crate::state::{ - ADDRESS_LP_WEIGHT, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS, OPEN_POSITIONS, + ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS_HISTORY, POSITIONS, POSITION_ID_COUNTER, }; use crate::ContractError; +/// Fills a position. If the position already exists, it will be expanded. Otherwise, a new position is created. pub(crate) fn fill_position( deps: DepsMut, env: Env, info: MessageInfo, - params: PositionParams, + identifier: Option, + lp_asset: Asset, + unlocking_duration: u64, + receiver: Option, ) -> Result { let config = CONFIG.load(deps.storage)?; - // validate unbonding duration - validate_unbonding_duration(&config, ¶ms)?; + // validate unlocking duration + validate_unlocking_duration(&config, unlocking_duration)?; let mut messages: Vec = vec![]; + //todo this will change when we remove the cw20 token support // ensure the lp tokens are transferred to the contract. If the LP is a cw20 token, creates // a transfer message - let transfer_token_msg = validate_funds_sent( - &deps.as_ref(), - env.clone(), - info.clone(), - params.clone().lp_asset, - )?; + let transfer_token_msg = + validate_funds_sent(&deps.as_ref(), env.clone(), info.clone(), lp_asset.clone())?; + //todo this will go away after we remove the cw20 token support if let Some(transfer_token_msg) = transfer_token_msg { messages.push(transfer_token_msg.into()); } // if receiver was not specified, default to the sender of the message. - let receiver = params + let receiver = receiver .clone() - .receiver .map(|r| deps.api.addr_validate(&r)) .transpose()? .map(|receiver| MessageInfo { @@ -47,47 +51,47 @@ pub(crate) fn fill_position( }) .unwrap_or_else(|| info.clone()); - // check if there's an existing position with the given `unbonding_time`, get the index of it - // on the vector - let position_index = OPEN_POSITIONS - .may_load(deps.storage, &receiver.sender)? - .unwrap_or_default() - .iter() - .enumerate() - .find(|(_, position)| position.unbonding_duration == params.unbonding_duration) - .map(|(index, _)| index); - - // update the position - OPEN_POSITIONS.update::<_, ContractError>(deps.storage, &receiver.sender, |positions| { - let mut positions = positions.unwrap_or_default(); - - // if the position exists, expand it. Otherwise, create a new position by adding it to the vector - match position_index { - Some(index) => { - // Update the existing position at the given index - if let Some(pos) = positions.get_mut(index) { - if pos.lp_asset.info != params.lp_asset.info { - return Err(ContractError::AssetMismatch); - } - pos.lp_asset.amount = - pos.lp_asset.amount.checked_add(params.lp_asset.amount)?; - } - } - None => { - positions.push(Position { - lp_asset: params.clone().lp_asset, - unbonding_duration: params.unbonding_duration, - }); - } - } + // check if there's an existing open position with the given `identifier` + let position = if let Some(identifier) = identifier { + // there is a position + POSITIONS.may_load(deps.storage, &identifier)? + } else { + // there is no position + None + }; + + if let Some(mut position) = position { + // there is a position, fill it + position.lp_asset.amount = position.lp_asset.amount.checked_add(lp_asset.amount)?; - Ok(positions) - })?; + POSITIONS.save(deps.storage, &position.identifier, &position)?; + } else { + // No position found, create a new one + let identifier = (POSITION_ID_COUNTER + .may_load(deps.storage)? + .unwrap_or_default() + + 1u64) + .to_string(); + POSITION_ID_COUNTER.update(deps.storage, |id| Ok(id.unwrap_or_default() + 1u64))?; + + POSITIONS.save( + deps.storage, + &identifier, + &Position { + identifier, + lp_asset, + unlocking_duration, + open: true, + expiring_at: None, + receiver: receiver.sender.clone(), + }, + )?; + } // Update weights for the LP and the user - update_weights(deps, &receiver, ¶ms)?; + update_weights(deps, &receiver, &lp_asset, unlocking_duration, true)?; - let action = match position_index { + let action = match position { Some(_) => "expand_position", None => "open_position", }; @@ -95,58 +99,179 @@ pub(crate) fn fill_position( Ok(Response::default().add_attributes(vec![ ("action", action.to_string()), ("receiver", receiver.sender.to_string()), - ("params", params.clone().to_string()), + ("lp_asset", lp_asset.clone().to_string()), + ("unlocking_duration", unlocking_duration.to_string()), ])) } +/// Closes an existing position +pub(crate) fn close_position( + deps: DepsMut, + env: Env, + info: MessageInfo, + identifier: String, + lp_asset: Option, +) -> Result { + cw_utils::nonpayable(&info)?; + + //todo do this validation to see if there are pending rewards + //query and check if the user has pending rewards + // let rewards_query_result = get_rewards(deps.as_ref(), info.sender.clone().into_string()); + // if let Ok(rewards_response) = rewards_query_result { + // // can't close a position if there are pending rewards + // if !rewards_response.rewards.is_empty() { + // return Err(ContractError::PendingRewards); + // } + // } + + let mut position = POSITIONS + .may_load(deps.storage, &identifier)? + .ok_or(ContractError::NoPositionFound { identifier })?; + + if position.receiver != info.sender { + return Err(ContractError::Unauthorized); + } + + let mut attributes = vec![ + ("action", "close_position".to_string()), + ("receiver", info.sender.to_string()), + ("identifier", identifier.clone().to_string()), + ]; + + // check if it's gonna be closed in full or partially + if let Some(lp_asset) = lp_asset { + // close position partially + + // check if the lp_asset requested to close matches the lp_asset of the position + if position.lp_asset.info != lp_asset.info { + return Err(ContractError::AssetMismatch); + } + + position.lp_asset.amount = position.lp_asset.amount.saturating_sub(lp_asset.amount); + + // add the partial closing position to the storage + let expires_at = env + .block + .time + .plus_seconds(position.unlocking_duration) + .seconds(); + + let identifier = (POSITION_ID_COUNTER + .may_load(deps.storage)? + .unwrap_or_default() + + 1u64) + .to_string(); + POSITION_ID_COUNTER.update(deps.storage, |id| Ok(id.unwrap_or_default() + 1u64))?; + + let partial_position = Position { + identifier, + lp_asset, + unlocking_duration: position.unlocking_duration, + open: false, + expiring_at: Some(expires_at), + receiver: position.receiver.clone(), + }; + POSITIONS.save(deps.storage, &identifier, &partial_position)?; + + attributes.push(("close_in_full", false.to_string())); + } else { + // close position in full + position.open = false; + attributes.push(("close_in_full", true.to_string())); + } + + POSITIONS.save(deps.storage, &identifier, &position)?; + + Ok(Response::default().add_attributes(attributes)) +} + +/// Withdraws the given position. The position needs to have expired. +pub(crate) fn withdraw_position( + deps: DepsMut, + env: Env, + info: MessageInfo, + identifier: String, +) -> Result { + // let expired_positions = get_expired_positions_by_receiver( + // deps.storage, + // env.block.time, + // info.sender.clone().into_string(), + // )?; + + let position = POSITIONS + .may_load(deps.storage, &identifier)? + .ok_or(ContractError::NoPositionFound { identifier })?; + + // check if this position is eligible for withdrawal + if position.receiver != info.sender || position.open || position.expiring_at.is_none() { + return Err(ContractError::Unauthorized); + } + + if position.expiring_at.unwrap() > env.block.time.seconds() { + return Err(ContractError::PositionNotExpired); + } + + let withdraw_message = position.lp_asset.into_msg(position.receiver.clone())?; + + POSITIONS.remove(deps.storage, &identifier); + + Ok(Response::default() + .add_attributes(vec![ + ("action", "withdraw_position".to_string()), + ("receiver", info.sender.to_string()), + ("identifier", identifier.clone().to_string()), + ]) + .add_message(withdraw_message)) +} + /// Updates the weights when managing a position fn update_weights( deps: DepsMut, receiver: &MessageInfo, - params: &PositionParams, + lp_asset: &Asset, + unlocking_duration: u64, + fill: bool, ) -> Result<(), ContractError> { - let weight = calculate_weight(params)?; - LP_WEIGHTS.update::<_, StdError>( - deps.storage, - ¶ms.lp_asset.info.as_bytes(), - |lp_weight| Ok(lp_weight.unwrap_or_default().checked_add(weight)?), - )?; - - // update the user's weight for this LP - let mut address_lp_weight = ADDRESS_LP_WEIGHT - .may_load( - deps.storage, - (&receiver.sender, ¶ms.lp_asset.info.as_bytes()), - )? - .unwrap_or_default(); - address_lp_weight = address_lp_weight.checked_add(weight)?; - ADDRESS_LP_WEIGHT.save( - deps.storage, - (&receiver.sender, ¶ms.lp_asset.info.as_bytes()), - &address_lp_weight, - )?; - let config = CONFIG.load(deps.storage)?; let current_epoch = white_whale::epoch_manager::common::get_current_epoch( deps.as_ref(), config.epoch_manager_addr.clone().into_string(), )?; + let weight = calculate_weight(lp_asset, unlocking_duration)?; + + let (_, mut lp_weight) = get_latest_lp_weight(deps.storage, lp_asset.info.as_bytes())?; + + if fill { + // filling position + lp_weight = lp_weight.checked_add(weight)?; + } else { + // closing position + lp_weight = lp_weight.saturating_sub(weight)?; + } + + LP_WEIGHTS_HISTORY.update::<_, StdError>( + deps.storage, + (lp_asset.info.as_bytes(), current_epoch.id + 1u64), + |_| Ok(lp_weight), + )?; + + // update the user's weight for this LP + let (_, mut address_lp_weight) = get_latest_address_weight(deps.storage, &receiver.sender)?; + + if fill { + // filling position + address_lp_weight = address_lp_weight.checked_add(weight)?; + } else { + // closing position + address_lp_weight = address_lp_weight.saturating_sub(weight)?; + } + ADDRESS_LP_WEIGHT_HISTORY.update::<_, StdError>( deps.storage, - (&receiver.sender, current_epoch + 1u64), + (&receiver.sender, current_epoch.id + 1u64), |_| Ok(address_lp_weight), )?; Ok(()) } - -/// Closes an existing position -pub(crate) fn close_position( - deps: DepsMut, - env: Env, - info: MessageInfo, - unbonding_duration: u64, -) -> Result { - Ok(Response::default().add_attributes(vec![("action", "close_position".to_string())])) -} diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index 342ffa9aa..e01a41436 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -1,9 +1,13 @@ -use cosmwasm_std::{to_json_binary, Decimal256, Deps, Env, MessageInfo, Uint128, WasmMsg}; +use cosmwasm_std::{ + to_json_binary, Addr, Decimal256, Deps, Env, MessageInfo, Order, StdResult, Storage, Uint128, + WasmMsg, +}; use cw_utils::PaymentError; -use white_whale::incentive_manager::PositionParams; +use white_whale::incentive_manager::{EpochId, PositionParams}; use white_whale::pool_network::asset::{Asset, AssetInfo}; +use crate::state::{ADDRESS_LP_WEIGHT, ADDRESS_LP_WEIGHT_HISTORY, LP_WEIGHTS_HISTORY}; use crate::ContractError; /// Validates that the message sender has sent the tokens to the contract. @@ -72,31 +76,32 @@ const SECONDS_IN_DAY: u64 = 86400; const SECONDS_IN_YEAR: u64 = 31556926; /// Calculates the weight size for a user filling a position -pub fn calculate_weight(params: &PositionParams) -> Result { - if !(SECONDS_IN_DAY..=SECONDS_IN_YEAR).contains(¶ms.unbonding_duration) { - return Err(ContractError::InvalidWeight { - unbonding_duration: params.unbonding_duration, - }); +pub fn calculate_weight( + lp_asset: &Asset, + unlocking_duration: u64, +) -> Result { + if !(SECONDS_IN_DAY..=SECONDS_IN_YEAR).contains(&unlocking_duration) { + return Err(ContractError::InvalidWeight { unlocking_duration }); } // store in Uint128 form for later - let amount_uint = params.lp_asset.amount; + let amount_uint = lp_asset.amount; // interpolate between [(86400, 1), (15778463, 5), (31556926, 16)] // note that 31556926 is not exactly one 365-day year, but rather one Earth rotation year // similarly, 15778463 is not 1/2 a 365-day year, but rather 1/2 a one Earth rotation year // first we need to convert into decimals - let unbonding_duration = Decimal256::from_atomics(params.unbonding_duration, 0).unwrap(); - let amount = Decimal256::from_atomics(params.lp_asset.amount, 0).unwrap(); + let unlocking_duration = Decimal256::from_atomics(unlocking_duration, 0).unwrap(); + let amount = Decimal256::from_atomics(lp_asset.amount, 0).unwrap(); - let unbonding_duration_squared = unbonding_duration.checked_pow(2)?; - let unbonding_duration_mul = - unbonding_duration_squared.checked_mul(Decimal256::raw(109498841))?; - let unbonding_duration_part = - unbonding_duration_mul.checked_div(Decimal256::raw(7791996353100889432894))?; + let unlocking_duration_squared = unlocking_duration.checked_pow(2)?; + let unlocking_duration_mul = + unlocking_duration_squared.checked_mul(Decimal256::raw(109498841))?; + let unlocking_duration_part = + unlocking_duration_mul.checked_div(Decimal256::raw(7791996353100889432894))?; - let next_part = unbonding_duration + let next_part = unlocking_duration .checked_mul(Decimal256::raw(249042009202369))? .checked_div(Decimal256::raw(7791996353100889432894))?; @@ -104,7 +109,7 @@ pub fn calculate_weight(params: &PositionParams) -> Result Result Result<(EpochId, Uint128), ContractError> { + Ok(ADDRESS_LP_WEIGHT_HISTORY + .prefix(address) + .range(storage, None, None, Order::Descending) + .take(1) // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. + .collect::>()?) +} + +/// Gets the latest available weight snapshot recorded for the given lp. +pub fn get_latest_lp_weight( + storage: &dyn Storage, + lp_asset_key: &[u8], +) -> Result<(EpochId, Uint128), ContractError> { + Ok(LP_WEIGHTS_HISTORY + .prefix(lp_asset_key) + .range(storage, None, None, Order::Descending) + .take(1) // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. + .collect::>()?) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs index 39da526a6..718bed737 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs @@ -1,2 +1,3 @@ pub mod commands; mod helpers; +mod queries; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/queries.rs b/contracts/liquidity_hub/incentive-manager/src/position/queries.rs new file mode 100644 index 000000000..4a005774f --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/position/queries.rs @@ -0,0 +1,24 @@ +use crate::state::{CONFIG, LAST_CLAIMED_EPOCH}; +use crate::ContractError; +use cosmwasm_std::{Addr, Deps}; +use white_whale::epoch_manager::common::get_current_epoch; +use white_whale::incentive_manager::RewardsResponse; + +pub(crate) fn get_rewards(deps: Deps, address: Addr) -> Result { + let config = CONFIG.load(deps.storage)?; + let current_epoch = get_current_epoch(deps.as_ref(), config.epoch_manager_addr.into_string())?; + + let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, &address)?; + + // Check if the user ever claimed before + if let Some(last_claimed_epoch) = last_claimed_epoch { + // if the last claimed epoch is the same as the current epoch, then there is nothing to claim + if current_epoch.id == last_claimed_epoch { + return Ok(RewardsResponse { rewards: vec![] }); + } + } + + let mut rewards = vec![]; + + Ok(RewardsResponse { rewards }) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 70d5b45d8..309c6c175 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -1,14 +1,63 @@ -use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; -use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cosmwasm_std::{Addr, Order, StdResult, Storage, Timestamp, Uint128}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; -use white_whale::incentive_manager::{Config, EpochId, Incentive, Position}; +use white_whale::incentive_manager::{ + Config, EpochId, Incentive, PartialClosingPosition, Position, +}; use white_whale::pool_network::asset::AssetInfo; +use white_whale::vault_manager::Vault; use crate::ContractError; -// Contract's config +/// Contract's config pub const CONFIG: Item = Item::new("config"); +/// An monotonically increasing counter to generate unique position identifiers. +pub const POSITION_ID_COUNTER: Item = Item::new("position_id_counter"); + +/// The positions that a user has. Positions can be open or closed. +pub const POSITIONS: Map<&String, Position> = Map::new("positions"); + +//todo maybe this will be needed? +/// The key is a tuple of (user_address, lp_asset_info as bytes, expiration block). +pub const POSITIONX: IndexedMap = IndexedMap::new( + "positionx", + PositionIndexes { + lp_asset: MultiIndex::new( + |_pk, p| p.lp_asset.to_string(), + "positions", + "positions__lp_asset", + ), + receiver: MultiIndex::new( + |_pk, p| p.receiver.to_string(), + "positions", + "positions__receiver", + ), + open: MultiIndex::new(|_pk, p| p.open, "positions", "positions__open"), + }, +); + +pub struct PositionIndexes<'a> { + pub lp_asset: MultiIndex<'a, String, Position, String>, + pub receiver: MultiIndex<'a, String, Position, String>, + pub open: MultiIndex<'a, bool, Position, String>, +} + +impl<'a> IndexList for PositionIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.lp_asset, &self.receiver, &self.open]; + Box::new(v.into_iter()) + } +} + +/// The positions that are being closed (partially) that were part of an open position. +/// For example, if a user had an open position of 10 LP with an unlocking period of a month, and then closes 3 LP of that position, +/// this map will store the 3 LP position that is being closed, so the unlocking clock of a month starts ticking for those 3 LP tokens. +/// The remaining 7 LP tokens will still be part of the original open position, stored in the POSITIONS map. +/// The key is a tuple of (user_address, lp_asset_info as bytes, expiration block). +pub const PARTIAL_CLOSING_POSITIONS: Map<(&Addr, &[u8], u64), PartialClosingPosition> = + Map::new("partial_closing_positions"); + /// All open positions that a user have. Open positions accumulate rewards, and a user can have /// multiple open positions active at once. pub const OPEN_POSITIONS: Map<&Addr, Vec> = Map::new("open_positions"); @@ -23,6 +72,9 @@ pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch /// The total weight (sum of all individual weights) of an LP asset pub const LP_WEIGHTS: Map<&[u8], Uint128> = Map::new("lp_weights"); +/// The history of total weight (sum of all individual weights) of an LP asset at a given epoch +pub const LP_WEIGHTS_HISTORY: Map<(&[u8], EpochId), Uint128> = Map::new("lp_weights_history"); + /// The weights for individual accounts pub const ADDRESS_LP_WEIGHT: Map<(&Addr, &[u8]), Uint128> = Map::new("address_lp_weight"); @@ -143,3 +195,32 @@ pub fn get_incentive_by_identifier( .may_load(storage, incentive_identifier.clone())? .ok_or(ContractError::NonExistentIncentive {}) } + +/// Gets the positions of the given receiver. +pub fn get_expired_positions_by_receiver( + storage: &dyn Storage, + time: Timestamp, + receiver: String, +) -> StdResult> { + let limit = MAX_LIMIT as usize; + + POSITIONX + .idx + .receiver + .prefix(receiver) + .range(storage, None, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, position) = item?; + position + }) + .filter(|position| { + if position.expiring_at.is_none() || position.open { + return false; + } + + let expiring_at = position.expiring_at.unwrap(); + expiring_at > time.seconds() + }) + .collect() +} diff --git a/packages/white-whale/src/common.rs b/packages/white-whale/src/common.rs index fa1fbe90e..9e971497a 100644 --- a/packages/white-whale/src/common.rs +++ b/packages/white-whale/src/common.rs @@ -25,11 +25,9 @@ pub fn update_ownership( info: MessageInfo, action: Action, ) -> Result { - Ok( - cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map(|ownership| { - Response::default() - .add_attribute("action", "update_ownership") - .add_attributes(ownership.into_attributes()) - })?, - ) + cw_ownable::update_ownership(deps, &env.block, &info.sender, action).map(|ownership| { + Response::default() + .add_attribute("action", "update_ownership") + .add_attributes(ownership.into_attributes()) + }) } diff --git a/packages/white-whale/src/constants.rs b/packages/white-whale/src/constants.rs index 39820253f..c8b46adba 100644 --- a/packages/white-whale/src/constants.rs +++ b/packages/white-whale/src/constants.rs @@ -1 +1,2 @@ pub const LP_SYMBOL: &str = "uLP"; +pub const DAY_SECONDS: u64 = 86400u64; diff --git a/packages/white-whale/src/epoch_manager/common.rs b/packages/white-whale/src/epoch_manager/common.rs index e875bb5d6..7e065d108 100644 --- a/packages/white-whale/src/epoch_manager/common.rs +++ b/packages/white-whale/src/epoch_manager/common.rs @@ -1,12 +1,28 @@ -use cosmwasm_std::{Deps, StdResult}; +use cosmwasm_std::{Deps, StdError, StdResult, Timestamp}; -use crate::epoch_manager::epoch_manager::{EpochResponse, QueryMsg}; +use crate::constants::DAY_SECONDS; +use crate::epoch_manager::epoch_manager::{Epoch, EpochResponse, QueryMsg}; /// Queries the current epoch from the epoch manager contract -pub fn get_current_epoch(deps: Deps, epoch_manager_addr: String) -> StdResult { +pub fn get_current_epoch(deps: Deps, epoch_manager_addr: String) -> StdResult { let epoch_response: EpochResponse = deps .querier .query_wasm_smart(epoch_manager_addr, &QueryMsg::CurrentEpoch {})?; - Ok(epoch_response.epoch.id) + Ok(epoch_response.epoch) +} + +/// Validates that the given epoch has not expired, i.e. not more than 24 hours have passed since the start of the epoch. +pub fn validate_epoch(epoch: &Epoch, current_time: Timestamp) -> StdResult<()> { + if current_time + .minus_seconds(epoch.start_time.seconds()) + .seconds() + < DAY_SECONDS + { + return Err(StdError::generic_err( + "Current epoch has expired, please wait for the next epoch to start.", + )); + } + + Ok(()) } diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index cd5ca3b21..201e80042 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::fmt::Formatter; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; use crate::epoch_manager::hooks::EpochChangedHookMsg; @@ -23,10 +23,12 @@ pub struct InstantiateMsg { pub max_concurrent_incentives: u32, /// New incentives are allowed to start up to `current_epoch + start_epoch_buffer` into the future. pub max_incentive_epoch_buffer: u32, - /// The minimum amount of time that a user can bond their tokens for. In seconds. - pub min_unbonding_duration: u64, - /// The maximum amount of time that a user can bond their tokens for. In seconds. - pub max_unbonding_duration: u64, + /// The minimum amount of time that a user can lock their tokens for. In seconds. + pub min_unlocking_duration: u64, + /// The maximum amount of time that a user can lock their tokens for. In seconds. + pub max_unlocking_duration: u64, + /// The penalty for unlocking a position before the unlocking duration finishes. In percentage. + pub emergency_unlock_penalty: Decimal, } /// The execution messages @@ -74,10 +76,12 @@ pub struct Config { pub max_concurrent_incentives: u32, /// The maximum amount of epochs in the future a new incentive is allowed to start in. pub max_incentive_epoch_buffer: u32, - /// The minimum amount of time that a user can bond their tokens for. In seconds. - pub min_unbonding_duration: u64, - /// The maximum amount of time that a user can bond their tokens for. In seconds. - pub max_unbonding_duration: u64, + /// The minimum amount of time that a user can lock their tokens for. In seconds. + pub min_unlocking_duration: u64, + /// The maximum amount of time that a user can lock their tokens for. In seconds. + pub max_unlocking_duration: u64, + /// The penalty for unlocking a position before the unlocking duration finishes. In percentage. + pub emergency_unlock_penalty: Decimal, } /// Parameters for creating incentive @@ -96,7 +100,7 @@ pub struct IncentiveParams { /// The asset to be distributed in this incentive. pub incentive_asset: Asset, /// If set, it will be used to identify the incentive. - pub incentive_indentifier: Option, + pub incentive_identifier: Option, } #[cw_serde] @@ -120,35 +124,53 @@ pub enum PositionAction { /// Fills a position. If the position doesn't exist, it opens it. If it exists already, /// it expands it given the sender opened the original position and the params are correct. Fill { - /// The parameters for the position to fill. - params: PositionParams, + /// The identifier of the position. + identifier: Option, + /// The asset to add to the position. + lp_asset: Asset, + /// The time it takes in seconds to unlock this position. This is used to identify the position to fill. + unlocking_duration: u64, + /// The receiver for the position. + /// If left empty, defaults to the message sender. + receiver: Option, }, /// Closes an existing position. The position stops earning incentive rewards. Close { - /// The unbonding duration of the position to close. - unbonding_duration: u64, + /// The identifier of the position. + identifier: String, + /// The asset to add to the position. If not set, the position will be closed in full. If not, it could be partially closed. + lp_asset: Option, + }, + /// Withdraws the LP tokens from a position after the position has been closed and the unlocking duration has passed. + Withdraw { + /// The identifier of the position. + identifier: String, }, } /// Parameters for creating incentive #[cw_serde] pub struct PositionParams { + /// The identifier of the position. + pub position_identifier: Option, /// The asset to add to the position. pub lp_asset: Asset, - /// The unbond completion timestamp to identify the position to add to. In seconds. - pub unbonding_duration: u64, + /// The time it takes in seconds to unlock this position. This is used to identify the position to fill. + pub unlocking_duration: u64, /// The receiver for the position. /// If left empty, defaults to the message sender. pub receiver: Option, + /// The action to perform on the position, either Fill or Close. + pub position_action: PositionAction, } impl std::fmt::Display for PositionParams { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "lp_asset: {}, unbonding_duration: {}, receiver: {}", + "lp_asset: {}, unlocking_duration: {}, receiver: {}", self.lp_asset, - self.unbonding_duration, + self.unlocking_duration, self.receiver.as_ref().unwrap_or(&"".to_string()) ) } @@ -210,8 +232,31 @@ pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; /// Represents an LP position. #[cw_serde] pub struct Position { + /// The identifier of the position. + pub identifier: String, /// The amount of LP tokens that are put up to earn incentives. pub lp_asset: Asset, - /// Represents the amount of time in seconds the user must wait after unbonding for the LP tokens to be released. - pub unbonding_duration: u64, + /// Represents the amount of time in seconds the user must wait after unlocking for the LP tokens to be released. + pub unlocking_duration: u64, + /// If true, the position is open. If false, the position is closed. + pub open: bool, + /// The block height at which the position, after being closed, can be withdrawn. + pub expiring_at: Option, + /// The owner of the position. + pub receiver: Addr, +} + +/// Represents an LP position that is being partially closed. +#[cw_serde] +pub struct PartialClosingPosition { + /// The amount of LP tokens that are being closed. + pub lp_asset: Asset, + /// The block height at which the position is completely closed and can be withdrawn. + pub expiring_at: u64, +} + +#[cw_serde] +pub struct RewardsResponse { + /// The rewards that is available to a user if they executed the `claim` function at this point. + pub rewards: Vec, } From 095a1f9dbd13a6fb98fe8e13ce123a326860fe12 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 22 Feb 2024 12:01:42 +0000 Subject: [PATCH 12/35] chore: refactoring --- .../incentive-manager/src/error.rs | 3 + .../incentive-manager/src/helpers.rs | 4 +- .../src/incentive/commands.rs | 7 +- .../src/incentive/queries.rs | 16 ----- .../incentive-manager/src/manager/commands.rs | 21 ++++-- .../src/position/commands.rs | 31 +++------ .../incentive-manager/src/position/helpers.rs | 2 +- .../incentive-manager/src/state.rs | 64 +++++++------------ packages/white-whale/src/incentive_manager.rs | 37 ----------- 9 files changed, 58 insertions(+), 127 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index b4d1db043..e60fcd9a9 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -102,6 +102,9 @@ pub enum ContractError { #[error("Incentive start timestamp is too far into the future")] IncentiveStartTooFar, + #[error("The incentive has already ended, can't be expanded")] + IncentiveAlreadyEnded {}, + #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] MigrateInvalidVersion { new_version: Version, diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index e98703d92..6a3c6d3b3 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -2,9 +2,7 @@ use std::cmp::Ordering; use cosmwasm_std::{wasm_execute, BankMsg, Coin, CosmosMsg, Decimal, Deps, Env, MessageInfo}; -use white_whale::incentive_manager::{ - Config, IncentiveParams, PositionParams, DEFAULT_INCENTIVE_DURATION, -}; +use white_whale::incentive_manager::{Config, IncentiveParams, DEFAULT_INCENTIVE_DURATION}; use white_whale::pool_network::asset::{Asset, AssetInfo}; use crate::ContractError; diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index 41d79efa9..29639a9d6 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; -use crate::state::{CONFIG, OPEN_POSITIONS}; +use crate::state::{get_open_positions_by_receiver, CONFIG, POSITIONS}; use crate::ContractError; /// Claims pending rewards for incentives where the user has LP @@ -8,9 +8,8 @@ pub(crate) fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result, ContractError> { - Ok(INCENTIVES - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()? - .into_iter() - .filter(|((start_epoch, _), _)| start_epoch <= epoch) - .map(|(_, flow)| flow) - .collect::>()) -} diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 3a031d0c8..6fe95dd81 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Storage, Uint128}; +use cosmwasm_std::{ + ensure, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, Storage, Uint128, +}; use white_whale::epoch_manager::common::validate_epoch; use white_whale::epoch_manager::hooks::EpochChangedHookMsg; @@ -191,7 +193,7 @@ fn close_incentives( for mut incentive in incentives { // remove the incentive from the storage - INCENTIVES.remove(storage, incentive.identifier.clone())?; + INCENTIVES.remove(storage, &incentive.identifier)?; // return the available asset, i.e. the amount that hasn't been claimed incentive.incentive_asset.amount = incentive @@ -210,7 +212,7 @@ fn expand_incentive( deps: DepsMut, env: Env, info: MessageInfo, - incentive: Incentive, + mut incentive: Incentive, params: IncentiveParams, ) -> Result { // only the incentive owner can expand it @@ -218,7 +220,17 @@ fn expand_incentive( return Err(ContractError::Unauthorized {}); } - // validate the params are correct and the incentive can actually be expanded + let config = CONFIG.load(deps.storage)?; + let current_epoch = white_whale::epoch_manager::common::get_current_epoch( + deps.as_ref(), + config.epoch_manager_addr.clone().into_string(), + )?; + + // check if the incentive has already ended, can't be expanded + ensure!( + incentive.end_epoch >= current_epoch.id, + ContractError::IncentiveAlreadyEnded {} + ); Ok(Response::default().add_attributes(vec![ ("action", "close_incentive".to_string()), @@ -226,6 +238,7 @@ fn expand_incentive( ])) } +//todo maybe this is not necessary /// EpochChanged hook implementation. Updates the LP_WEIGHTS. pub(crate) fn on_epoch_changed( diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 3aa985449..455615a4e 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -8,7 +8,8 @@ use crate::position::helpers::{ calculate_weight, get_latest_address_weight, get_latest_lp_weight, validate_funds_sent, }; use crate::state::{ - ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS_HISTORY, POSITIONS, POSITION_ID_COUNTER, + get_position, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS_HISTORY, POSITIONS, + POSITION_ID_COUNTER, }; use crate::ContractError; @@ -52,13 +53,7 @@ pub(crate) fn fill_position( .unwrap_or_else(|| info.clone()); // check if there's an existing open position with the given `identifier` - let position = if let Some(identifier) = identifier { - // there is a position - POSITIONS.may_load(deps.storage, &identifier)? - } else { - // there is no position - None - }; + let position = get_position(deps.storage, identifier)?; if let Some(mut position) = position { // there is a position, fill it @@ -124,8 +119,7 @@ pub(crate) fn close_position( // } // } - let mut position = POSITIONS - .may_load(deps.storage, &identifier)? + let mut position = get_position(deps.storage, Some(identifier.clone()))? .ok_or(ContractError::NoPositionFound { identifier })?; if position.receiver != info.sender { @@ -135,7 +129,7 @@ pub(crate) fn close_position( let mut attributes = vec![ ("action", "close_position".to_string()), ("receiver", info.sender.to_string()), - ("identifier", identifier.clone().to_string()), + ("identifier", identifier.to_string()), ]; // check if it's gonna be closed in full or partially @@ -192,14 +186,9 @@ pub(crate) fn withdraw_position( info: MessageInfo, identifier: String, ) -> Result { - // let expired_positions = get_expired_positions_by_receiver( - // deps.storage, - // env.block.time, - // info.sender.clone().into_string(), - // )?; - - let position = POSITIONS - .may_load(deps.storage, &identifier)? + cw_utils::nonpayable(&info)?; + + let position = get_position(deps.storage, Some(identifier.clone()))? .ok_or(ContractError::NoPositionFound { identifier })?; // check if this position is eligible for withdrawal @@ -213,7 +202,7 @@ pub(crate) fn withdraw_position( let withdraw_message = position.lp_asset.into_msg(position.receiver.clone())?; - POSITIONS.remove(deps.storage, &identifier); + POSITIONS.remove(deps.storage, &identifier)?; Ok(Response::default() .add_attributes(vec![ @@ -224,7 +213,7 @@ pub(crate) fn withdraw_position( .add_message(withdraw_message)) } -/// Updates the weights when managing a position +/// Updates the weights when managing a position. Computes what the weight is gonna be in the next epoch. fn update_weights( deps: DepsMut, receiver: &MessageInfo, diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index e01a41436..61a0dac32 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -7,7 +7,7 @@ use cw_utils::PaymentError; use white_whale::incentive_manager::{EpochId, PositionParams}; use white_whale::pool_network::asset::{Asset, AssetInfo}; -use crate::state::{ADDRESS_LP_WEIGHT, ADDRESS_LP_WEIGHT_HISTORY, LP_WEIGHTS_HISTORY}; +use crate::state::{ADDRESS_LP_WEIGHT_HISTORY, LP_WEIGHTS_HISTORY}; use crate::ContractError; /// Validates that the message sender has sent the tokens to the contract. diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 309c6c175..192773748 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -16,12 +16,9 @@ pub const CONFIG: Item = Item::new("config"); pub const POSITION_ID_COUNTER: Item = Item::new("position_id_counter"); /// The positions that a user has. Positions can be open or closed. -pub const POSITIONS: Map<&String, Position> = Map::new("positions"); - -//todo maybe this will be needed? -/// The key is a tuple of (user_address, lp_asset_info as bytes, expiration block). -pub const POSITIONX: IndexedMap = IndexedMap::new( - "positionx", +/// The key is the position identifier +pub const POSITIONS: IndexedMap<&String, Position, PositionIndexes> = IndexedMap::new( + "positions", PositionIndexes { lp_asset: MultiIndex::new( |_pk, p| p.lp_asset.to_string(), @@ -50,34 +47,12 @@ impl<'a> IndexList for PositionIndexes<'a> { } } -/// The positions that are being closed (partially) that were part of an open position. -/// For example, if a user had an open position of 10 LP with an unlocking period of a month, and then closes 3 LP of that position, -/// this map will store the 3 LP position that is being closed, so the unlocking clock of a month starts ticking for those 3 LP tokens. -/// The remaining 7 LP tokens will still be part of the original open position, stored in the POSITIONS map. -/// The key is a tuple of (user_address, lp_asset_info as bytes, expiration block). -pub const PARTIAL_CLOSING_POSITIONS: Map<(&Addr, &[u8], u64), PartialClosingPosition> = - Map::new("partial_closing_positions"); - -/// All open positions that a user have. Open positions accumulate rewards, and a user can have -/// multiple open positions active at once. -pub const OPEN_POSITIONS: Map<&Addr, Vec> = Map::new("open_positions"); - -/// All closed positions that users have. Closed positions don't accumulate rewards, and the -/// underlying tokens are claimable after `unbonding_duration`. -pub const CLOSED_POSITIONS: Map<&Addr, Vec> = Map::new("closed_positions"); - /// The last epoch an address claimed rewards pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch"); -/// The total weight (sum of all individual weights) of an LP asset -pub const LP_WEIGHTS: Map<&[u8], Uint128> = Map::new("lp_weights"); - /// The history of total weight (sum of all individual weights) of an LP asset at a given epoch pub const LP_WEIGHTS_HISTORY: Map<(&[u8], EpochId), Uint128> = Map::new("lp_weights_history"); -/// The weights for individual accounts -pub const ADDRESS_LP_WEIGHT: Map<(&Addr, &[u8]), Uint128> = Map::new("address_lp_weight"); - /// The address lp weight history, i.e. how much lp weight an address had at a given epoch pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, EpochId), Uint128> = Map::new("address_lp_weight_history"); @@ -86,7 +61,7 @@ pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, EpochId), Uint128> = pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); /// Incentives map -pub const INCENTIVES: IndexedMap = IndexedMap::new( +pub const INCENTIVES: IndexedMap<&String, Incentive, IncentiveIndexes> = IndexedMap::new( "incentives", IncentiveIndexes { lp_asset: MultiIndex::new( @@ -192,19 +167,33 @@ pub fn get_incentive_by_identifier( incentive_identifier: &String, ) -> Result { INCENTIVES - .may_load(storage, incentive_identifier.clone())? + .may_load(storage, incentive_identifier)? .ok_or(ContractError::NonExistentIncentive {}) } +/// Gets a position given its identifier. If the position is not found with the given identifier, it returns None. +pub fn get_position( + storage: &dyn Storage, + identifier: Option, +) -> StdResult> { + if let Some(identifier) = identifier { + // there is a position + POSITIONS.may_load(storage, &identifier) + } else { + // there is no position + Ok(None) + } +} + +//todo think of the limit when claiming rewards /// Gets the positions of the given receiver. -pub fn get_expired_positions_by_receiver( +pub fn get_open_positions_by_receiver( storage: &dyn Storage, - time: Timestamp, receiver: String, ) -> StdResult> { let limit = MAX_LIMIT as usize; - POSITIONX + POSITIONS .idx .receiver .prefix(receiver) @@ -214,13 +203,6 @@ pub fn get_expired_positions_by_receiver( let (_, position) = item?; position }) - .filter(|position| { - if position.expiring_at.is_none() || position.open { - return false; - } - - let expiring_at = position.expiring_at.unwrap(); - expiring_at > time.seconds() - }) + .filter(|position| position.open) .collect() } diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index 201e80042..539649501 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -148,34 +148,6 @@ pub enum PositionAction { }, } -/// Parameters for creating incentive -#[cw_serde] -pub struct PositionParams { - /// The identifier of the position. - pub position_identifier: Option, - /// The asset to add to the position. - pub lp_asset: Asset, - /// The time it takes in seconds to unlock this position. This is used to identify the position to fill. - pub unlocking_duration: u64, - /// The receiver for the position. - /// If left empty, defaults to the message sender. - pub receiver: Option, - /// The action to perform on the position, either Fill or Close. - pub position_action: PositionAction, -} - -impl std::fmt::Display for PositionParams { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "lp_asset: {}, unlocking_duration: {}, receiver: {}", - self.lp_asset, - self.unlocking_duration, - self.receiver.as_ref().unwrap_or(&"".to_string()) - ) - } -} - // type for the epoch id pub type EpochId = u64; @@ -246,15 +218,6 @@ pub struct Position { pub receiver: Addr, } -/// Represents an LP position that is being partially closed. -#[cw_serde] -pub struct PartialClosingPosition { - /// The amount of LP tokens that are being closed. - pub lp_asset: Asset, - /// The block height at which the position is completely closed and can be withdrawn. - pub expiring_at: u64, -} - #[cw_serde] pub struct RewardsResponse { /// The rewards that is available to a user if they executed the `claim` function at this point. From d2ddb11abdcf3efea853649b01459e95c78abbca Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 23 Feb 2024 11:35:05 +0000 Subject: [PATCH 13/35] feat: emergency withdrawal --- .../incentive-manager/src/contract.rs | 7 +-- .../src/position/commands.rs | 48 ++++++++++++++++--- packages/white-whale/src/incentive_manager.rs | 2 + 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 5764449ed..faff5082f 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -124,9 +124,10 @@ pub fn execute( identifier, lp_asset, } => close_position(deps, env, info, identifier, lp_asset), - PositionAction::Withdraw { identifier } => { - withdraw_position(deps, env, info, identifier) - } + PositionAction::Withdraw { + identifier, + emergency_unlock, + } => withdraw_position(deps, env, info, identifier, emergency_unlock), }, } } diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 455615a4e..22af69322 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -185,22 +185,56 @@ pub(crate) fn withdraw_position( env: Env, info: MessageInfo, identifier: String, + emergency_unlock: Option, ) -> Result { cw_utils::nonpayable(&info)?; - let position = get_position(deps.storage, Some(identifier.clone()))? + let mut position = get_position(deps.storage, Some(identifier.clone()))? .ok_or(ContractError::NoPositionFound { identifier })?; - // check if this position is eligible for withdrawal - if position.receiver != info.sender || position.open || position.expiring_at.is_none() { + if position.receiver != info.sender { return Err(ContractError::Unauthorized); } - if position.expiring_at.unwrap() > env.block.time.seconds() { - return Err(ContractError::PositionNotExpired); + let mut messages: Vec = vec![]; + + // check if the emergency unlock is requested, will pull the whole position out whether it's open, closed or expired, paying the penalty + if emergency_unlock.is_some() && emergency_unlock.unwrap() { + let emergency_unlock_penalty = CONFIG.load(deps.storage)?.emergency_unlock_penalty; + + let penalty_fee = position.lp_asset.amount * emergency_unlock_penalty; + + let penalty = Asset { + info: position.lp_asset.info.clone(), + amount: penalty_fee, + }; + + let whale_lair_addr = CONFIG.load(deps.storage)?.whale_lair_addr; + + // send penalty to whale lair for distribution + messages.push(white_whale::whale_lair::fill_rewards_msg( + whale_lair_addr.into_string(), + vec![penalty], + )?); + + // subtract the penalty from the original position + position.lp_asset.amount = position.lp_asset.amount.saturating_sub(penalty_fee); + } else { + // check if this position is eligible for withdrawal + if position.open || position.expiring_at.is_none() { + return Err(ContractError::Unauthorized); + } + + if position.expiring_at.unwrap() < env.block.time.seconds() { + return Err(ContractError::PositionNotExpired); + } } - let withdraw_message = position.lp_asset.into_msg(position.receiver.clone())?; + // sanity check + if !position.lp_asset.amount.is_zero() { + // withdraw the remaining LP tokens + messages.push(position.lp_asset.into_msg(position.receiver.clone())?); + } POSITIONS.remove(deps.storage, &identifier)?; @@ -210,7 +244,7 @@ pub(crate) fn withdraw_position( ("receiver", info.sender.to_string()), ("identifier", identifier.clone().to_string()), ]) - .add_message(withdraw_message)) + .add_messages(messages)) } /// Updates the weights when managing a position. Computes what the weight is gonna be in the next epoch. diff --git a/packages/white-whale/src/incentive_manager.rs b/packages/white-whale/src/incentive_manager.rs index 539649501..986948868 100644 --- a/packages/white-whale/src/incentive_manager.rs +++ b/packages/white-whale/src/incentive_manager.rs @@ -145,6 +145,8 @@ pub enum PositionAction { Withdraw { /// The identifier of the position. identifier: String, + /// Whether to unlock the position in an emergency. If set to true, the position will be unlocked immediately, but with a penalty. + emergency_unlock: Option, }, } From 48329e5eaf961ac2054cdea75d8acb93fb0184f2 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 13 Mar 2024 17:49:23 +0000 Subject: [PATCH 14/35] chore: finish removing cw20 support from incentive manager --- .../incentive-manager/src/contract.rs | 13 +- .../incentive-manager/src/error.rs | 12 +- .../incentive-manager/src/helpers.rs | 148 ++++++------------ .../src/incentive/commands.rs | 17 +- .../incentive-manager/src/manager/commands.rs | 68 ++++---- .../src/position/commands.rs | 71 ++++----- .../incentive-manager/src/position/helpers.rs | 81 +--------- .../incentive-manager/src/position/queries.rs | 4 +- .../incentive-manager/src/state.rs | 6 +- .../white-whale-std/src/incentive_manager.rs | 19 +-- 10 files changed, 155 insertions(+), 284 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index b1463fa50..67c78abca 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -54,7 +54,7 @@ pub fn instantiate( Ok(Response::default().add_attributes(vec![ ("action", "instantiate".to_string()), - ("owner", msg.owner.to_string()), + ("owner", msg.owner), ("epoch_manager_addr", config.epoch_manager_addr.to_string()), ("whale_lair_addr", config.whale_lair_addr.to_string()), ("create_flow_fee", config.create_incentive_fee.to_string()), @@ -108,18 +108,9 @@ pub fn execute( ExecuteMsg::ManagePosition { action } => match action { PositionAction::Fill { identifier, - lp_asset, - unlocking_duration, - receiver, - } => fill_position( - deps, - env, - info, - identifier, - lp_asset, unlocking_duration, receiver, - ), + } => fill_position(deps, info, identifier, unlocking_duration, receiver), PositionAction::Close { identifier, lp_asset, diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 5d65fe66b..b176c9d87 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -65,9 +65,6 @@ pub enum ContractError { min: u128, }, - #[error("The asset sent is not supported for fee payments")] - FeeAssetNotSupported, - #[error("Incentive creation fee was not included")] IncentiveFeeMissing, @@ -87,11 +84,8 @@ pub enum ContractError { required_amount: Uint128, }, - #[error("Specified incentive asset was not transferred")] - IncentiveAssetNotSent, - #[error("The end epoch for this incentive is invalid")] - InvalidEndEpoch {}, + InvalidEndEpoch, #[error("Incentive end timestamp was set to a time in the past")] IncentiveEndsInPast, @@ -103,7 +97,7 @@ pub enum ContractError { IncentiveStartTooFar, #[error("The incentive has already ended, can't be expanded")] - IncentiveAlreadyEnded {}, + IncentiveAlreadyEnded, #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] MigrateInvalidVersion { @@ -154,7 +148,7 @@ pub enum ContractError { #[error("The emergency unlock penalty provided is invalid")] InvalidEmergencyUnlockPenalty, - #[error("There're pending rewards to be claimed before this action can be executed")] + #[error("There are pending rewards to be claimed before this action can be executed")] PendingRewards, } diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 8f4851c29..9d30dbf9e 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -1,11 +1,8 @@ use std::cmp::Ordering; -use cosmwasm_std::{ - wasm_execute, BankMsg, Coin, CosmosMsg, Decimal, Deps, Env, MessageInfo, Uint128, -}; +use cosmwasm_std::{ensure, BankMsg, Coin, CosmosMsg, Decimal, MessageInfo, Uint128}; use white_whale_std::incentive_manager::{Config, IncentiveParams, DEFAULT_INCENTIVE_DURATION}; -use white_whale_std::pool_network::asset::{Asset, AssetInfo}; use crate::ContractError; @@ -39,38 +36,31 @@ pub(crate) fn process_incentive_creation_fee( // if the user is paying more than the incentive_creation_fee, check if it's trying to create // an incentive with the same asset as the incentive_creation_fee. // otherwise, refund the difference - match params.incentive_asset.info.clone() { - AssetInfo::Token { .. } => {} - AssetInfo::NativeToken { - denom: incentive_asset_denom, - } => { - if incentive_creation_fee.denom == incentive_asset_denom { - // check if the amounts add up, i.e. the fee + incentive asset = paid amount. That is because the incentive asset - // and the creation fee asset are the same, all go in the info.funds of the transaction - if params - .incentive_asset - .amount - .checked_add(incentive_creation_fee.amount)? - != paid_fee_amount - { - return Err(ContractError::AssetMismatch); - } - } else { - let refund_amount = - paid_fee_amount.saturating_sub(incentive_creation_fee.amount); - if refund_amount > Uint128::zero() { - messages.push( - BankMsg::Send { - to_address: info.sender.clone().into_string(), - amount: vec![Coin { - amount: refund_amount, - denom: incentive_creation_fee.denom.clone(), - }], - } - .into(), - ); + if incentive_creation_fee.denom == params.incentive_asset.denom { + // check if the amounts add up, i.e. the fee + incentive asset = paid amount. That is because the incentive asset + // and the creation fee asset are the same, all go in the info.funds of the transaction + + ensure!( + params + .incentive_asset + .amount + .checked_add(incentive_creation_fee.amount)? + == paid_fee_amount, + ContractError::AssetMismatch + ); + } else { + let refund_amount = paid_fee_amount.saturating_sub(incentive_creation_fee.amount); + if refund_amount > Uint128::zero() { + messages.push( + BankMsg::Send { + to_address: info.sender.clone().into_string(), + amount: vec![Coin { + amount: refund_amount, + denom: incentive_creation_fee.denom.clone(), + }], } - } + .into(), + ); } } } @@ -88,73 +78,33 @@ pub(crate) fn process_incentive_creation_fee( /// Asserts the incentive asset was sent correctly, considering the incentive creation fee if applicable. /// Returns a vector of messages to be sent (applies only when the incentive asset is a CW20 token) pub(crate) fn assert_incentive_asset( - deps: Deps, - env: &Env, info: &MessageInfo, incentive_creation_fee: &Coin, - params: &mut IncentiveParams, -) -> Result, ContractError> { - let mut messages: Vec = vec![]; - - match params.incentive_asset.info.clone() { - AssetInfo::NativeToken { - denom: incentive_asset_denom, - } => { - let coin_sent = info - .funds - .iter() - .find(|sent| sent.denom == incentive_asset_denom) - .ok_or(ContractError::AssetMismatch)?; - - if incentive_creation_fee.denom != incentive_asset_denom { - if coin_sent.amount != params.incentive_asset.amount { - return Err(ContractError::AssetMismatch); - } - } else { - if params - .incentive_asset - .amount - .checked_add(incentive_creation_fee.amount)? - != coin_sent.amount - { - return Err(ContractError::AssetMismatch); - } - } - } - //todo remove - AssetInfo::Token { - contract_addr: incentive_asset_contract_addr, - } => { - // make sure the incentive asset has enough allowance - let allowance: cw20::AllowanceResponse = deps.querier.query_wasm_smart( - incentive_asset_contract_addr.clone(), - &cw20::Cw20QueryMsg::Allowance { - owner: info.sender.clone().into_string(), - spender: env.contract.address.clone().into_string(), - }, - )?; - - if allowance.allowance < params.incentive_asset.amount { - return Err(ContractError::AssetMismatch); - } - - // create the transfer message to the incentive manager - messages.push( - wasm_execute( - env.contract.address.clone().into_string(), - &cw20::Cw20ExecuteMsg::TransferFrom { - owner: info.sender.clone().into_string(), - recipient: env.contract.address.clone().into_string(), - amount: params.incentive_asset.amount, - }, - vec![], - )? - .into(), - ); - } + params: &IncentiveParams, +) -> Result<(), ContractError> { + let coin_sent = info + .funds + .iter() + .find(|sent| sent.denom == params.incentive_asset.denom) + .ok_or(ContractError::AssetMismatch)?; + + if incentive_creation_fee.denom != params.incentive_asset.denom { + ensure!( + coin_sent.amount == params.incentive_asset.amount, + ContractError::AssetMismatch + ); + } else { + ensure!( + params + .incentive_asset + .amount + .checked_add(incentive_creation_fee.amount)? + == coin_sent.amount, + ContractError::AssetMismatch + ); } - Ok(messages) + Ok(()) } /// Validates the incentive epochs. Returns a tuple of (start_epoch, end_epoch) for the incentive. @@ -167,7 +117,7 @@ pub(crate) fn validate_incentive_epochs( let end_epoch = params.end_epoch.unwrap_or( current_epoch .checked_add(DEFAULT_INCENTIVE_DURATION) - .ok_or(ContractError::InvalidEndEpoch {})?, + .ok_or(ContractError::InvalidEndEpoch)?, ); // ensure the incentive is set to end in a future epoch diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index 8a2ba52f9..f05d557b2 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -1,25 +1,30 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; -use crate::state::{get_open_positions_by_receiver, CONFIG, POSITIONS}; +use crate::state::{get_open_positions_by_receiver, CONFIG}; use crate::ContractError; /// Claims pending rewards for incentives where the user has LP -pub(crate) fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { +pub(crate) fn claim( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { cw_utils::nonpayable(&info)?; // check if the user has any open LP positions - let mut open_positions = - get_open_positions_by_receiver(deps.storage, info.sender.clone().into_string())?; + let open_positions = get_open_positions_by_receiver(deps.storage, info.sender.into_string())?; if open_positions.is_empty() { return Err(ContractError::NoOpenPositions); } let config = CONFIG.load(deps.storage)?; - let current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( + let _current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( deps.as_ref(), - config.epoch_manager_addr.clone().into_string(), + config.epoch_manager_addr.into_string(), )?; + //todo complete this + Ok(Response::default().add_attributes(vec![("action", "claim".to_string())])) } diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 7cf457c2a..fb562289d 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - ensure, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdError, Storage, - Uint128, + ensure, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdError, + Storage, Uint128, }; use white_whale_std::epoch_manager::common::validate_epoch; @@ -27,12 +27,12 @@ pub(crate) fn fill_incentive( // exists and if the sender is allow to refill it, otherwise create a new incentive if let Some(incentive_indentifier) = params.clone().incentive_identifier { let incentive_result = get_incentive_by_identifier(deps.storage, &incentive_indentifier); - match incentive_result { + + if let Ok(incentive) = incentive_result { // the incentive exists, try to expand it - Ok(incentive) => return expand_incentive(deps, env, info, incentive, params), - // the incentive does not exist, try to create it - Err(_) => {} + return expand_incentive(deps, env, info, incentive, params); } + // the incentive does not exist, try to create it } // if no identifier was passed in the params or if the incentive does not exist, try to create the incentive @@ -50,7 +50,7 @@ fn create_incentive( let config = CONFIG.load(deps.storage)?; let incentives = get_incentives_by_lp_asset( deps.storage, - ¶ms.lp_asset, + ¶ms.lp_denom, None, Some(config.max_concurrent_incentives), )?; @@ -99,13 +99,7 @@ fn create_incentive( } // verify the incentive asset was sent - messages.append(&mut assert_incentive_asset( - deps.as_ref(), - &env, - &info, - &incentive_creation_fee, - &mut params, - )?); + assert_incentive_asset(&info, &incentive_creation_fee, ¶ms)?; // assert epoch params are correctly set let (start_epoch, end_epoch) = validate_incentive_epochs( @@ -122,10 +116,11 @@ fn create_incentive( .unwrap_or(incentive_id.to_string()); // make sure another incentive with the same identifier doesn't exist - match get_incentive_by_identifier(deps.storage, &incentive_identifier) { - Ok(_) => return Err(ContractError::IncentiveAlreadyExists {}), - Err(_) => {} // the incentive does not exist, all good, continue - } + ensure!( + get_incentive_by_identifier(deps.storage, &incentive_identifier).is_err(), + ContractError::IncentiveAlreadyExists + ); + // the incentive does not exist, all good, continue // create the incentive let incentive = Incentive { @@ -135,7 +130,7 @@ fn create_incentive( //emitted_tokens: HashMap::new(), curve: params.curve.unwrap_or(Curve::Linear), incentive_asset: params.incentive_asset, - lp_asset: params.lp_asset, + lp_denom: params.lp_denom, owner: info.sender, claimed_amount: Uint128::zero(), expansion_history: Default::default(), @@ -149,7 +144,7 @@ fn create_incentive( ("end_epoch", incentive.end_epoch.to_string()), ("curve", incentive.curve.to_string()), ("incentive_asset", incentive.incentive_asset.to_string()), - ("lp_asset", incentive.lp_asset.to_string()), + ("lp_denom", incentive.lp_denom), ])) } @@ -169,12 +164,12 @@ pub(crate) fn close_incentive( config.epoch_manager_addr.into_string(), )?; - let mut incentive = get_incentive_by_identifier(deps.storage, &incentive_identifier)?; + let incentive = get_incentive_by_identifier(deps.storage, &incentive_identifier)?; if !(!incentive.is_expired(current_epoch.id) && (incentive.owner == info.sender || cw_ownable::is_owner(deps.storage, &info.sender)?)) { - return Err(ContractError::Unauthorized {}); + return Err(ContractError::Unauthorized); } Ok(Response::default() @@ -201,8 +196,14 @@ fn close_incentives( .incentive_asset .amount .saturating_sub(incentive.claimed_amount); - //TODO remake this into_msg since we are getting rid of the Asset struct in V2 - messages.push(incentive.incentive_asset.into_msg(incentive.owner)?); + + messages.push( + BankMsg::Send { + to_address: incentive.owner.into_string(), + amount: vec![incentive.incentive_asset], + } + .into(), + ); } Ok(messages) @@ -211,10 +212,10 @@ fn close_incentives( /// Expands an incentive with the given params fn expand_incentive( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, - mut incentive: Incentive, - params: IncentiveParams, + incentive: Incentive, + _params: IncentiveParams, ) -> Result { // only the incentive owner can expand it if incentive.owner != info.sender { @@ -230,9 +231,11 @@ fn expand_incentive( // check if the incentive has already ended, can't be expanded ensure!( incentive.end_epoch >= current_epoch.id, - ContractError::IncentiveAlreadyEnded {} + ContractError::IncentiveAlreadyEnded ); + //todo complete this + Ok(Response::default().add_attributes(vec![ ("action", "close_incentive".to_string()), ("incentive_identifier", incentive.identifier), @@ -244,12 +247,10 @@ fn expand_incentive( pub(crate) fn on_epoch_changed( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, - msg: EpochChangedHookMsg, + _msg: EpochChangedHookMsg, ) -> Result { - cw_utils::nonpayable(&info)?; - let config = CONFIG.load(deps.storage)?; // only the epoch manager can trigger this @@ -261,9 +262,12 @@ pub(crate) fn on_epoch_changed( // // msg.current_epoch + //todo complete this + Ok(Response::default()) } +#[allow(clippy::too_many_arguments)] /// Updates the configuration of the contract pub(crate) fn update_config( deps: DepsMut, diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index a0f48bb7a..42e822035 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -1,12 +1,11 @@ -use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError}; +use cosmwasm_std::{ + ensure, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, +}; use white_whale_std::incentive_manager::Position; -use white_whale_std::pool_network::asset::Asset; use crate::helpers::validate_unlocking_duration; -use crate::position::helpers::{ - calculate_weight, get_latest_address_weight, get_latest_lp_weight, validate_funds_sent, -}; +use crate::position::helpers::{calculate_weight, get_latest_address_weight, get_latest_lp_weight}; use crate::state::{ get_position, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS_HISTORY, POSITIONS, POSITION_ID_COUNTER, @@ -16,34 +15,20 @@ use crate::ContractError; /// Fills a position. If the position already exists, it will be expanded. Otherwise, a new position is created. pub(crate) fn fill_position( deps: DepsMut, - env: Env, info: MessageInfo, identifier: Option, - lp_asset: Asset, unlocking_duration: u64, receiver: Option, ) -> Result { let config = CONFIG.load(deps.storage)?; + let lp_asset = cw_utils::one_coin(&info)?; + // validate unlocking duration validate_unlocking_duration(&config, unlocking_duration)?; - let mut messages: Vec = vec![]; - - //todo this will change when we remove the cw20 token support - // ensure the lp tokens are transferred to the contract. If the LP is a cw20 token, creates - // a transfer message - let transfer_token_msg = - validate_funds_sent(&deps.as_ref(), env.clone(), info.clone(), lp_asset.clone())?; - - //todo this will go away after we remove the cw20 token support - if let Some(transfer_token_msg) = transfer_token_msg { - messages.push(transfer_token_msg.into()); - } - // if receiver was not specified, default to the sender of the message. let receiver = receiver - .clone() .map(|r| deps.api.addr_validate(&r)) .transpose()? .map(|receiver| MessageInfo { @@ -57,9 +42,13 @@ pub(crate) fn fill_position( if let Some(ref mut position) = position { // there is a position, fill it - position.lp_asset.amount = position.lp_asset.amount.checked_add(lp_asset.amount)?; + ensure!( + position.lp_asset.denom == lp_asset.denom, + ContractError::AssetMismatch + ); - POSITIONS.save(deps.storage, &position.identifier, &position)?; + position.lp_asset.amount = position.lp_asset.amount.checked_add(lp_asset.amount)?; + POSITIONS.save(deps.storage, &position.identifier, position)?; } else { // No position found, create a new one let identifier = POSITION_ID_COUNTER @@ -84,7 +73,7 @@ pub(crate) fn fill_position( } // Update weights for the LP and the user - update_weights(deps, &receiver, &lp_asset.clone(), unlocking_duration, true)?; + update_weights(deps, &receiver, &lp_asset, unlocking_duration, true)?; let action = match position { Some(_) => "expand_position", @@ -105,7 +94,7 @@ pub(crate) fn close_position( env: Env, info: MessageInfo, identifier: String, - lp_asset: Option, + lp_asset: Option, ) -> Result { cw_utils::nonpayable(&info)?; @@ -140,9 +129,10 @@ pub(crate) fn close_position( // close position partially // check if the lp_asset requested to close matches the lp_asset of the position - if position.lp_asset.info != lp_asset.info { - return Err(ContractError::AssetMismatch); - } + ensure!( + lp_asset.denom == position.lp_asset.denom, + ContractError::AssetMismatch + ); position.lp_asset.amount = position.lp_asset.amount.saturating_sub(lp_asset.amount); @@ -209,15 +199,16 @@ pub(crate) fn withdraw_position( let penalty_fee = position.lp_asset.amount * emergency_unlock_penalty; - let penalty = Asset { - info: position.lp_asset.info.clone(), + let penalty = Coin { + denom: position.lp_asset.denom.to_string(), amount: penalty_fee, }; let whale_lair_addr = CONFIG.load(deps.storage)?.whale_lair_addr; // send penalty to whale lair for distribution - messages.push(white_whale_std::whale_lair::fill_rewards_msg( + //todo the whale lair needs to withdraw the LP tokens from the corresponding pool when this happens + messages.push(white_whale_std::whale_lair::fill_rewards_msg_coin( whale_lair_addr.into_string(), vec![penalty], )?); @@ -238,7 +229,13 @@ pub(crate) fn withdraw_position( // sanity check if !position.lp_asset.amount.is_zero() { // withdraw the remaining LP tokens - messages.push(position.lp_asset.into_msg(position.receiver)?); + messages.push( + BankMsg::Send { + to_address: position.receiver.to_string(), + amount: vec![position.lp_asset], + } + .into(), + ); } POSITIONS.remove(deps.storage, &identifier)?; @@ -247,7 +244,7 @@ pub(crate) fn withdraw_position( .add_attributes(vec![ ("action", "withdraw_position".to_string()), ("receiver", info.sender.to_string()), - ("identifier", identifier.to_string()), + ("identifier", identifier), ]) .add_messages(messages)) } @@ -256,19 +253,19 @@ pub(crate) fn withdraw_position( fn update_weights( deps: DepsMut, receiver: &MessageInfo, - lp_asset: &Asset, + lp_asset: &Coin, unlocking_duration: u64, fill: bool, ) -> Result<(), ContractError> { let config = CONFIG.load(deps.storage)?; let current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( deps.as_ref(), - config.epoch_manager_addr.clone().into_string(), + config.epoch_manager_addr.into_string(), )?; let weight = calculate_weight(lp_asset, unlocking_duration)?; - let (_, mut lp_weight) = get_latest_lp_weight(deps.storage, lp_asset.info.as_bytes())?; + let (_, mut lp_weight) = get_latest_lp_weight(deps.storage, lp_asset.denom.as_bytes())?; if fill { // filling position @@ -280,7 +277,7 @@ fn update_weights( LP_WEIGHTS_HISTORY.update::<_, StdError>( deps.storage, - (lp_asset.info.as_bytes(), current_epoch.id + 1u64), + (lp_asset.denom.as_bytes(), current_epoch.id + 1u64), |_| Ok(lp_weight), )?; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index 28d51b024..8ce721933 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -1,83 +1,15 @@ -use cosmwasm_std::{ - to_json_binary, Addr, Decimal256, Deps, Env, MessageInfo, Order, StdResult, Storage, Uint128, - WasmMsg, -}; -use cw_utils::PaymentError; +use cosmwasm_std::{Addr, Coin, Decimal256, Storage, Uint128}; use white_whale_std::incentive_manager::EpochId; -use white_whale_std::pool_network::asset::{Asset, AssetInfo}; -use crate::state::{ADDRESS_LP_WEIGHT_HISTORY, LP_WEIGHTS_HISTORY}; use crate::ContractError; -/// Validates that the message sender has sent the tokens to the contract. -/// In case the `lp_token` is a cw20 token, check if the sender set the specified `amount` as an -/// allowance for us to transfer for `lp_token`. -/// -/// If `lp_token` is a native token, check if the funds were sent in the [`MessageInfo`] struct. -/// -/// Returns the [`WasmMsg`] that will transfer the specified `amount` of the -/// `lp_token` to the contract. -pub fn validate_funds_sent( - deps: &Deps, - env: Env, - info: MessageInfo, - lp_asset: Asset, -) -> Result, ContractError> { - if lp_asset.amount.is_zero() { - return Err(ContractError::PaymentError(PaymentError::NoFunds {})); - } - - let send_lp_deposit_msg = match lp_asset.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 < lp_asset.amount { - return Err(ContractError::MissingPositionDeposit { - allowance_amount: allowance.allowance, - deposited_amount: lp_asset.amount, - }); - } - - // send the lp deposit to us - Some(WasmMsg::Execute { - contract_addr, - msg: to_json_binary(&cw20::Cw20ExecuteMsg::TransferFrom { - owner: info.sender.into_string(), - recipient: env.contract.address.into_string(), - amount: lp_asset.amount, - })?, - funds: vec![], - }) - } - AssetInfo::NativeToken { denom } => { - let paid_amount = cw_utils::must_pay(&info, &denom)?; - if paid_amount != lp_asset.amount { - return Err(ContractError::MissingPositionDepositNative { - desired_amount: lp_asset.amount, - paid_amount, - }); - } - // no message needed as native tokens are transferred together with the transaction - None - } - }; - - Ok(send_lp_deposit_msg) -} - const SECONDS_IN_DAY: u64 = 86400; const SECONDS_IN_YEAR: u64 = 31556926; /// Calculates the weight size for a user filling a position pub fn calculate_weight( - lp_asset: &Asset, + lp_asset: &Coin, unlocking_duration: u64, ) -> Result { if !(SECONDS_IN_DAY..=SECONDS_IN_YEAR).contains(&unlocking_duration) { @@ -125,8 +57,8 @@ pub fn calculate_weight( /// Gets the latest available weight snapshot recorded for the given address. pub fn get_latest_address_weight( - storage: &dyn Storage, - address: &Addr, + _storage: &dyn Storage, + _address: &Addr, ) -> Result<(EpochId, Uint128), ContractError> { //todo this will likely change with the new implementation of the claim function // Ok(ADDRESS_LP_WEIGHT_HISTORY @@ -140,10 +72,11 @@ pub fn get_latest_address_weight( /// Gets the latest available weight snapshot recorded for the given lp. pub fn get_latest_lp_weight( - storage: &dyn Storage, - lp_asset_key: &[u8], + _storage: &dyn Storage, + _lp_asset_key: &[u8], ) -> Result<(EpochId, Uint128), ContractError> { //todo this will likely change with the new implementation of the claim function + // perhaps the lp_asset_key can become a String instead of bytes? // Ok(LP_WEIGHTS_HISTORY // .prefix(lp_asset_key) // .range(storage, None, None, Order::Descending) diff --git a/contracts/liquidity_hub/incentive-manager/src/position/queries.rs b/contracts/liquidity_hub/incentive-manager/src/position/queries.rs index 93b381784..ad3ea1e20 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/queries.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/queries.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{Addr, Deps}; use white_whale_std::epoch_manager::common::get_current_epoch; use white_whale_std::incentive_manager::RewardsResponse; -pub(crate) fn get_rewards(deps: Deps, address: Addr) -> Result { +pub(crate) fn _get_rewards(deps: Deps, address: Addr) -> Result { let config = CONFIG.load(deps.storage)?; let current_epoch = get_current_epoch(deps, config.epoch_manager_addr.into_string())?; @@ -18,7 +18,7 @@ pub(crate) fn get_rewards(deps: Deps, address: Addr) -> Result = Indexed "incentives", IncentiveIndexes { lp_asset: MultiIndex::new( - |_pk, i| i.lp_asset.to_string(), + |_pk, i| i.lp_denom.to_string(), "incentives", "incentives__lp_asset", ), @@ -115,7 +115,7 @@ pub fn get_incentives( /// Gets incentives given an lp asset [AssetInfo] pub fn get_incentives_by_lp_asset( storage: &dyn Storage, - lp_asset: &AssetInfo, + lp_denom: &str, start_after: Option, limit: Option, ) -> StdResult> { @@ -125,7 +125,7 @@ pub fn get_incentives_by_lp_asset( INCENTIVES .idx .lp_asset - .prefix(lp_asset.to_string()) + .prefix(lp_denom.to_owned()) .range(storage, start, None, Order::Ascending) .take(limit) .map(|item| { diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index 356786a1f..582e46fd3 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -5,7 +5,6 @@ use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; use crate::epoch_manager::hooks::EpochChangedHookMsg; -use crate::pool_network::asset::{Asset, AssetInfo}; /// The instantiation message #[cw_serde] @@ -105,8 +104,8 @@ pub struct Config { /// Parameters for creating incentive #[cw_serde] pub struct IncentiveParams { - /// The LP asset to create the incentive for. - pub lp_asset: AssetInfo, + /// The LP asset denom to create the incentive for. + pub lp_denom: String, /// The epoch at which the incentive will start. If unspecified, it will start at the /// current epoch. pub start_epoch: Option, @@ -116,7 +115,7 @@ pub struct IncentiveParams { /// The type of distribution curve. If unspecified, the distribution will be linear. pub curve: Option, /// The asset to be distributed in this incentive. - pub incentive_asset: Asset, + pub incentive_asset: Coin, /// If set, it will be used to identify the incentive. pub incentive_identifier: Option, } @@ -144,8 +143,6 @@ pub enum PositionAction { Fill { /// The identifier of the position. identifier: Option, - /// The asset to add to the position. - lp_asset: Asset, /// The time it takes in seconds to unlock this position. This is used to identify the position to fill. unlocking_duration: u64, /// The receiver for the position. @@ -157,7 +154,7 @@ pub enum PositionAction { /// The identifier of the position. identifier: String, /// The asset to add to the position. If not set, the position will be closed in full. If not, it could be partially closed. - lp_asset: Option, + lp_asset: Option, }, /// Withdraws the LP tokens from a position after the position has been closed and the unlocking duration has passed. Withdraw { @@ -178,10 +175,10 @@ pub struct Incentive { pub identifier: String, /// The account which opened the incentive and can manage it. pub owner: Addr, - /// The LP asset to create the incentive for. - pub lp_asset: AssetInfo, + /// The LP asset denom to create the incentive for. + pub lp_denom: String, /// The asset the incentive was created to distribute. - pub incentive_asset: Asset, + pub incentive_asset: Coin, /// The amount of the `incentive_asset` that has been claimed so far. pub claimed_amount: Uint128, /// The type of curve the incentive has. @@ -227,7 +224,7 @@ pub struct Position { /// The identifier of the position. pub identifier: String, /// The amount of LP tokens that are put up to earn incentives. - pub lp_asset: Asset, + pub lp_asset: Coin, /// Represents the amount of time in seconds the user must wait after unlocking for the LP tokens to be released. pub unlocking_duration: u64, /// If true, the position is open. If false, the position is closed. From b29e311e15669754f68d83bec69c5a886d1b7516 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 26 Mar 2024 17:28:42 +0000 Subject: [PATCH 15/35] chore: prework on claim --- .../incentive-manager/src/error.rs | 11 +- .../incentive-manager/src/helpers.rs | 8 +- .../incentive-manager/src/manager/commands.rs | 123 ++++++++++++------ .../incentive-manager/src/manager/mod.rs | 12 -- .../src/position/commands.rs | 29 +++-- .../incentive-manager/src/position/helpers.rs | 58 +++++---- .../incentive-manager/src/state.rs | 6 +- .../terraswap_pair/src/migrations.rs | 19 ++- packages/white-whale-std/src/coin.rs | 8 ++ .../white-whale-std/src/incentive_manager.rs | 40 +++--- 10 files changed, 200 insertions(+), 114 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index b176c9d87..827fa082b 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - CheckedFromRatioError, ConversionOverflowError, DivideByZeroError, OverflowError, StdError, - Uint128, + CheckedFromRatioError, CheckedMultiplyFractionError, ConversionOverflowError, + DivideByZeroError, OverflowError, StdError, Uint128, }; use cw_ownable::OwnershipError; use cw_utils::PaymentError; @@ -30,6 +30,9 @@ pub enum ContractError { #[error("{0}")] CheckedFromRatioError(#[from] CheckedFromRatioError), + #[error("{0}")] + CheckedMultiplyFractionError(#[from] CheckedMultiplyFractionError), + #[error("{0}")] ConversionOverflowError(#[from] ConversionOverflowError), @@ -96,8 +99,8 @@ pub enum ContractError { #[error("Incentive start timestamp is too far into the future")] IncentiveStartTooFar, - #[error("The incentive has already ended, can't be expanded")] - IncentiveAlreadyEnded, + #[error("The incentive has already expired, can't be expanded")] + IncentiveAlreadyExpired, #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] MigrateInvalidVersion { diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 9d30dbf9e..cae5f6cc1 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -114,21 +114,21 @@ pub(crate) fn validate_incentive_epochs( max_incentive_epoch_buffer: u64, ) -> Result<(u64, u64), ContractError> { // assert epoch params are correctly set - let end_epoch = params.end_epoch.unwrap_or( + let preliminary_end_epoch = params.preliminary_end_epoch.unwrap_or( current_epoch .checked_add(DEFAULT_INCENTIVE_DURATION) .ok_or(ContractError::InvalidEndEpoch)?, ); // ensure the incentive is set to end in a future epoch - if current_epoch > end_epoch { + if current_epoch > preliminary_end_epoch { return Err(ContractError::IncentiveEndsInPast); } let start_epoch = params.start_epoch.unwrap_or(current_epoch); // ensure that start date is before end date - if start_epoch > end_epoch { + if start_epoch > preliminary_end_epoch { return Err(ContractError::IncentiveStartTimeAfterEndTime); } @@ -137,7 +137,7 @@ pub(crate) fn validate_incentive_epochs( return Err(ContractError::IncentiveStartTooFar); } - Ok((start_epoch, end_epoch)) + Ok((start_epoch, preliminary_end_epoch)) } //todo maybe move this to position helpers?? diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index fb562289d..e9998b78b 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -3,17 +3,19 @@ use cosmwasm_std::{ Storage, Uint128, }; +use white_whale_std::coin::{get_subdenom, is_factory_token}; use white_whale_std::epoch_manager::common::validate_epoch; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; +use white_whale_std::incentive_manager::MIN_INCENTIVE_AMOUNT; use white_whale_std::incentive_manager::{Curve, Incentive, IncentiveParams}; use crate::helpers::{ assert_incentive_asset, process_incentive_creation_fee, validate_emergency_unlock_penalty, validate_incentive_epochs, }; -use crate::manager::MIN_INCENTIVE_AMOUNT; use crate::state::{ get_incentive_by_identifier, get_incentives_by_lp_asset, CONFIG, INCENTIVES, INCENTIVE_COUNTER, + LP_WEIGHTS_HISTORY, }; use crate::ContractError; @@ -73,18 +75,20 @@ fn create_incentive( } // check if more incentives can be created for this particular LP asset - if incentives.len() == config.max_concurrent_incentives as usize { - return Err(ContractError::TooManyIncentives { + ensure!( + incentives.len() < config.max_concurrent_incentives as usize, + ContractError::TooManyIncentives { max: config.max_concurrent_incentives, - }); - } + } + ); // check the incentive is being created with a valid amount - if params.incentive_asset.amount < MIN_INCENTIVE_AMOUNT { - return Err(ContractError::InvalidIncentiveAmount { - min: MIN_INCENTIVE_AMOUNT.u128(), - }); - } + ensure!( + params.incentive_asset.amount >= MIN_INCENTIVE_AMOUNT, + ContractError::InvalidIncentiveAmount { + min: MIN_INCENTIVE_AMOUNT.u128() + } + ); let incentive_creation_fee = config.create_incentive_fee.clone(); @@ -102,7 +106,7 @@ fn create_incentive( assert_incentive_asset(&info, &incentive_creation_fee, ¶ms)?; // assert epoch params are correctly set - let (start_epoch, end_epoch) = validate_incentive_epochs( + let (start_epoch, preliminary_end_epoch) = validate_incentive_epochs( ¶ms, current_epoch.id, u64::from(config.max_incentive_epoch_buffer), @@ -122,26 +126,38 @@ fn create_incentive( ); // the incentive does not exist, all good, continue + // calculates the emission rate + let emission_rate = params + .incentive_asset + .amount + .checked_div_floor((preliminary_end_epoch.saturating_sub(start_epoch), 1u64))?; + // create the incentive let incentive = Incentive { identifier: incentive_identifier, start_epoch, - end_epoch, - //emitted_tokens: HashMap::new(), + preliminary_end_epoch, curve: params.curve.unwrap_or(Curve::Linear), incentive_asset: params.incentive_asset, lp_denom: params.lp_denom, owner: info.sender, claimed_amount: Uint128::zero(), - expansion_history: Default::default(), + emission_rate, + last_epoch_claimed: current_epoch.id - 1, }; + INCENTIVES.save(deps.storage, &incentive.identifier, &incentive)?; + Ok(Response::default().add_attributes(vec![ ("action", "create_incentive".to_string()), ("incentive_creator", incentive.owner.to_string()), ("incentive_identifier", incentive.identifier), ("start_epoch", incentive.start_epoch.to_string()), - ("end_epoch", incentive.end_epoch.to_string()), + ( + "preliminary_end_epoch", + incentive.preliminary_end_epoch.to_string(), + ), + ("emission_rate", emission_rate.to_string()), ("curve", incentive.curve.to_string()), ("incentive_asset", incentive.incentive_asset.to_string()), ("lp_denom", incentive.lp_denom), @@ -214,13 +230,11 @@ fn expand_incentive( deps: DepsMut, _env: Env, info: MessageInfo, - incentive: Incentive, - _params: IncentiveParams, + mut incentive: Incentive, + params: IncentiveParams, ) -> Result { // only the incentive owner can expand it - if incentive.owner != info.sender { - return Err(ContractError::Unauthorized {}); - } + ensure!(incentive.owner == info.sender, ContractError::Unauthorized); let config = CONFIG.load(deps.storage)?; let current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( @@ -228,43 +242,74 @@ fn expand_incentive( config.epoch_manager_addr.into_string(), )?; - // check if the incentive has already ended, can't be expanded + // check if the incentive has already expired, can't be expanded ensure!( - incentive.end_epoch >= current_epoch.id, - ContractError::IncentiveAlreadyEnded + incentive.is_expired(current_epoch.id), + ContractError::IncentiveAlreadyExpired ); - //todo complete this + // check that the asset sent matches the asset expected + ensure!( + incentive.incentive_asset.denom == params.incentive_asset.denom, + ContractError::AssetMismatch + ); + + // increase the total amount of the incentive + incentive.incentive_asset.amount = incentive + .incentive_asset + .amount + .checked_add(params.incentive_asset.amount)?; + INCENTIVES.save(deps.storage, &incentive.identifier, &incentive)?; Ok(Response::default().add_attributes(vec![ - ("action", "close_incentive".to_string()), + ("action", "expand_incentive".to_string()), ("incentive_identifier", incentive.identifier), + ("expanded_by", params.incentive_asset.to_string()), + ("total_incentive", incentive.incentive_asset.to_string()), ])) } -//todo maybe this is not necessary /// EpochChanged hook implementation. Updates the LP_WEIGHTS. - pub(crate) fn on_epoch_changed( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, - _msg: EpochChangedHookMsg, + msg: EpochChangedHookMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; - // only the epoch manager can trigger this - if info.sender != config.epoch_manager_addr { - return Err(ContractError::Unauthorized {}); - } - // - // LP_WEIGHTS_HISTORY. - // - // msg.current_epoch + ensure!( + info.sender == config.epoch_manager_addr, + ContractError::Unauthorized + ); - //todo complete this + // get all LP tokens and update the LP_WEIGHTS_HISTORY + let lp_assets = deps + .querier + .query_all_balances(env.contract.address)? + .into_iter() + .filter(|asset| { + if is_factory_token(asset.denom.as_str()) { + //todo remove this hardcoded uLP and point to the pool manager const + get_subdenom(asset.denom.as_str()) == "uLP" + } else { + false + } + }) + .collect::>(); + + for lp_asset in &lp_assets { + LP_WEIGHTS_HISTORY.save( + deps.storage, + (&lp_asset.denom, msg.current_epoch.id), + &lp_asset.amount, + )?; + } - Ok(Response::default()) + Ok(Response::default().add_attributes(vec![ + ("action", "on_epoch_changed".to_string()), + ("epoch", msg.current_epoch.to_string()), + ])) } #[allow(clippy::too_many_arguments)] diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs b/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs index f67251f77..82b6da3c0 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/mod.rs @@ -1,13 +1 @@ -use cosmwasm_std::Uint128; - pub mod commands; - -/// Minimum amount of an asset to create an incentive with -pub(crate) const MIN_INCENTIVE_AMOUNT: Uint128 = Uint128::new(1_000u128); -// If the end_epoch is not specified, the incentive will be expanded by DEFAULT_INCENTIVE_DURATION when -// the current epoch is within INCENTIVE_EXPANSION_BUFFER epochs from the end_epoch. -pub(crate) const INCENTIVE_EXPANSION_BUFFER: u64 = 5u64; -// An incentive can only be expanded for a maximum of INCENTIVE_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. -pub(crate) const INCENTIVE_EXPANSION_LIMIT: u64 = 180u64; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 42e822035..2fa8b54d6 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -90,7 +90,7 @@ pub(crate) fn fill_position( /// Closes an existing position pub(crate) fn close_position( - deps: DepsMut, + mut deps: DepsMut, env: Env, info: MessageInfo, identifier: String, @@ -114,9 +114,10 @@ pub(crate) fn close_position( }, )?; - if position.receiver != info.sender { - return Err(ContractError::Unauthorized); - } + ensure!( + position.receiver == info.sender, + ContractError::Unauthorized + ); let mut attributes = vec![ ("action", "close_position".to_string()), @@ -124,7 +125,7 @@ pub(crate) fn close_position( ("identifier", identifier.to_string()), ]; - // check if it's gonna be closed in full or partially + // check if it's going to be closed in full or partially if let Some(lp_asset) = lp_asset { // close position partially @@ -158,13 +159,20 @@ pub(crate) fn close_position( receiver: position.receiver.clone(), }; POSITIONS.save(deps.storage, &identifier.to_string(), &partial_position)?; - - attributes.push(("close_in_full", false.to_string())); } else { // close position in full position.open = false; - attributes.push(("close_in_full", true.to_string())); } + let close_in_full = !position.open; + attributes.push(("close_in_full", close_in_full.to_string())); + + update_weights( + deps.branch(), + &info, + &position.lp_asset, + position.unlocking_duration, + false, + )?; POSITIONS.save(deps.storage, &identifier, &position)?; @@ -265,7 +273,7 @@ fn update_weights( let weight = calculate_weight(lp_asset, unlocking_duration)?; - let (_, mut lp_weight) = get_latest_lp_weight(deps.storage, lp_asset.denom.as_bytes())?; + let (_, mut lp_weight) = get_latest_lp_weight(deps.storage, &lp_asset.denom)?; if fill { // filling position @@ -277,7 +285,7 @@ fn update_weights( LP_WEIGHTS_HISTORY.update::<_, StdError>( deps.storage, - (lp_asset.denom.as_bytes(), current_epoch.id + 1u64), + (&lp_asset.denom, current_epoch.id + 1u64), |_| Ok(lp_weight), )?; @@ -292,6 +300,7 @@ fn update_weights( address_lp_weight = address_lp_weight.saturating_sub(weight); } + //todo if the address weight is zero, remove it from the storage? ADDRESS_LP_WEIGHT_HISTORY.update::<_, StdError>( deps.storage, (&receiver.sender, current_epoch.id + 1u64), diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index 8ce721933..bb7f7e21a 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -1,7 +1,8 @@ -use cosmwasm_std::{Addr, Coin, Decimal256, Storage, Uint128}; +use cosmwasm_std::{Addr, Coin, Decimal256, Order, StdError, Storage, Uint128}; use white_whale_std::incentive_manager::EpochId; +use crate::state::{ADDRESS_LP_WEIGHT_HISTORY, LP_WEIGHTS_HISTORY}; use crate::ContractError; const SECONDS_IN_DAY: u64 = 86400; @@ -57,31 +58,44 @@ pub fn calculate_weight( /// Gets the latest available weight snapshot recorded for the given address. pub fn get_latest_address_weight( - _storage: &dyn Storage, - _address: &Addr, + storage: &dyn Storage, + address: &Addr, ) -> Result<(EpochId, Uint128), ContractError> { - //todo this will likely change with the new implementation of the claim function - // Ok(ADDRESS_LP_WEIGHT_HISTORY - // .prefix(address) - // .range(storage, None, None, Order::Descending) - // .take(1) // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. - // .collect::>()?); - // dummy value meanwhile - Ok((0, Uint128::zero())) + let result = ADDRESS_LP_WEIGHT_HISTORY + .prefix(address) + .range(storage, None, None, Order::Descending) + .take(1usize) + // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. + .next() + .transpose(); + + return_latest_weight(result) } /// Gets the latest available weight snapshot recorded for the given lp. pub fn get_latest_lp_weight( - _storage: &dyn Storage, - _lp_asset_key: &[u8], + storage: &dyn Storage, + lp_asset: &str, +) -> Result<(EpochId, Uint128), ContractError> { + let result = LP_WEIGHTS_HISTORY + .prefix(lp_asset) + .range(storage, None, None, Order::Descending) + .take(1usize) + // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. + .next() + .transpose(); + + return_latest_weight(result) +} + +/// Helper function to return the weight from the result. If the result is None, i.e. the weight +/// was not found in the map, it returns (0, 0). +fn return_latest_weight( + weight_result: Result, StdError>, ) -> Result<(EpochId, Uint128), ContractError> { - //todo this will likely change with the new implementation of the claim function - // perhaps the lp_asset_key can become a String instead of bytes? - // Ok(LP_WEIGHTS_HISTORY - // .prefix(lp_asset_key) - // .range(storage, None, None, Order::Descending) - // .take(1) // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. - // .collect::>()?) - // dummy value meanwhile - Ok((0, Uint128::zero())) + match weight_result { + Ok(Some(item)) => Ok(item), + Ok(None) => Ok((0u64, Uint128::zero())), + Err(std_err) => Err(std_err.into()), + } } diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 14db79033..5533b376d 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -16,7 +16,7 @@ pub const POSITION_ID_COUNTER: Item = Item::new("position_id_counter"); /// The positions that a user has. Positions can be open or closed. /// The key is the position identifier -pub const POSITIONS: IndexedMap<&String, Position, PositionIndexes> = IndexedMap::new( +pub const POSITIONS: IndexedMap<&str, Position, PositionIndexes> = IndexedMap::new( "positions", PositionIndexes { lp_asset: MultiIndex::new( @@ -50,7 +50,7 @@ impl<'a> IndexList for PositionIndexes<'a> { pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch"); /// The history of total weight (sum of all individual weights) of an LP asset at a given epoch -pub const LP_WEIGHTS_HISTORY: Map<(&[u8], EpochId), Uint128> = Map::new("lp_weights_history"); +pub const LP_WEIGHTS_HISTORY: Map<(&str, EpochId), Uint128> = Map::new("lp_weights_history"); /// The address lp weight history, i.e. how much lp weight an address had at a given epoch pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, EpochId), Uint128> = @@ -60,7 +60,7 @@ pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, EpochId), Uint128> = pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); /// Incentives map -pub const INCENTIVES: IndexedMap<&String, Incentive, IncentiveIndexes> = IndexedMap::new( +pub const INCENTIVES: IndexedMap<&str, Incentive, IncentiveIndexes> = IndexedMap::new( "incentives", IncentiveIndexes { lp_asset: MultiIndex::new( diff --git a/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs b/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs index 4c004bb62..18806e632 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs +++ b/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs @@ -194,8 +194,23 @@ pub fn migrate_to_v13x(deps: DepsMut) -> Result<(), StdError> { pub swap_fee: Fee, } - const CONFIG_V110: Item = Item::new("config"); - let config_v110 = CONFIG_V110.load(deps.storage)?; + const CONFIG_V110: Item = Item::new("leaderboard"); + let leaderboard = CONFIG_V110.load(deps.storage)?; + + let mut start_from: Option = None; + for (addr, amount) in leaderboard.iter() { + let leaderboard = deps.api.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: "guppy_furnace".to_string(), + msg: to_binary(&LeaderBoard { + start_from: start_from, + limit: 30, + })?, + }))?; + + LEADERBOARD.save(deps.storage, &"uguppy", &leaderboard)?; + + start_from = Some(leaderboard.last()?); + } // Add burn fee to config. Zero fee is used as default. let config = Config { diff --git a/packages/white-whale-std/src/coin.rs b/packages/white-whale-std/src/coin.rs index 818d89571..466edd7c8 100644 --- a/packages/white-whale-std/src/coin.rs +++ b/packages/white-whale-std/src/coin.rs @@ -99,6 +99,14 @@ pub fn is_factory_token(denom: &str) -> bool { true } +/// Gets the subdenom of a factory token. To be called after [is_factory_token] has been successful. +pub fn get_subdenom(denom: &str) -> &str { + denom + .splitn(3, '/') + .nth(2) + .expect("Expected at least three elements") +} + /// Builds the label for a factory token denom in such way that it returns a label like "factory/mig...xyz/123...456". /// Call after [crate::pool_network::asset::is_factory_token] has been successful fn get_factory_token_label(denom: &str) -> StdResult { diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index 582e46fd3..b611e8ec9 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; @@ -109,9 +107,9 @@ pub struct IncentiveParams { /// The epoch at which the incentive will start. If unspecified, it will start at the /// current epoch. pub start_epoch: Option, - /// The epoch at which the incentive should end. If unspecified, the incentive will default to end at - /// 14 epochs from the current one. - pub end_epoch: Option, + /// The epoch at which the incentive should preliminarily end (if it's not expanded). If + /// unspecified, the incentive will default to end at 14 epochs from the current one. + pub preliminary_end_epoch: Option, /// The type of distribution curve. If unspecified, the distribution will be linear. pub curve: Option, /// The asset to be distributed in this incentive. @@ -181,23 +179,26 @@ pub struct Incentive { pub incentive_asset: Coin, /// The amount of the `incentive_asset` that has been claimed so far. pub claimed_amount: Uint128, + /// The amount of the `incentive_asset` that is to be distributed every epoch. + pub emission_rate: Uint128, /// The type of curve the incentive has. pub curve: Curve, /// The epoch at which the incentive starts. pub start_epoch: EpochId, - /// The epoch at which the incentive ends. - pub end_epoch: EpochId, - /// 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 incentive is expanded. - pub expansion_history: BTreeMap, + /// The epoch at which the incentive will preliminary end (in case it's not expanded). + pub preliminary_end_epoch: EpochId, + /// The last epoch this incentive was claimed. + pub last_epoch_claimed: EpochId, } impl Incentive { - /// Returns true if the incentive is expired at the given epoch. - pub fn is_expired(&self, epoch: u64) -> bool { - epoch > self.end_epoch + DEFAULT_INCENTIVE_DURATION + /// Returns true if the incentive is expired + pub fn is_expired(&self, epoch_id: EpochId) -> bool { + self.incentive_asset + .amount + .saturating_sub(self.claimed_amount) + < MIN_INCENTIVE_AMOUNT + || epoch_id >= self.last_epoch_claimed + DEFAULT_INCENTIVE_DURATION } } @@ -215,9 +216,6 @@ impl std::fmt::Display for Curve { } } -/// Default incentive duration in epochs -pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; - /// Represents an LP position. #[cw_serde] pub struct Position { @@ -240,3 +238,9 @@ pub struct RewardsResponse { /// The rewards that is available to a user if they executed the `claim` function at this point. pub rewards: Vec, } + +/// Minimum amount of an asset to create an incentive with +pub const MIN_INCENTIVE_AMOUNT: Uint128 = Uint128::new(1_000u128); + +/// Default incentive duration in epochs +pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; From ab8012f746b5bb3a1fd4066f5178a187230fcaa6 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 27 Mar 2024 16:25:08 +0000 Subject: [PATCH 16/35] feat(incentive_manager): claim implementation --- .../incentive-manager/src/contract.rs | 2 +- .../incentive-manager/src/error.rs | 3 + .../src/incentive/commands.rs | 290 +++++++++++++++++- .../incentive-manager/src/incentive/mod.rs | 3 + .../src/incentive/tests/helpers.rs | 11 + .../src/incentive/tests/mod.rs | 2 + .../src/incentive/tests/rewards.rs | 161 ++++++++++ .../incentive-manager/src/position/queries.rs | 4 +- .../incentive-manager/src/state.rs | 38 +++ packages/white-whale-std/src/coin.rs | 26 +- .../white-whale-std/src/incentive_manager.rs | 16 +- 11 files changed, 533 insertions(+), 23 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 67c78abca..8745a36e8 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -104,7 +104,7 @@ pub fn execute( ExecuteMsg::EpochChangedHook(msg) => { manager::commands::on_epoch_changed(deps, env, info, msg) } - ExecuteMsg::Claim => incentive::commands::claim(deps, env, info), + ExecuteMsg::Claim => incentive::commands::claim(deps, info), ExecuteMsg::ManagePosition { action } => match action { PositionAction::Fill { identifier, diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 827fa082b..ecdca7250 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -102,6 +102,9 @@ pub enum ContractError { #[error("The incentive has already expired, can't be expanded")] IncentiveAlreadyExpired, + #[error("The incentive doesn't have enough funds to pay out the reward")] + IncentiveExhausted, + #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] MigrateInvalidVersion { new_version: Version, diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index f05d557b2..cf2fc2a1f 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -1,30 +1,290 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; +use std::collections::HashMap; -use crate::state::{get_open_positions_by_receiver, CONFIG}; +use cosmwasm_std::{ + ensure, Addr, BankMsg, Coin, CosmosMsg, Deps, DepsMut, MessageInfo, Response, Storage, Uint128, +}; + +use white_whale_std::coin::aggregate_coins; +use white_whale_std::incentive_manager::{EpochId, Incentive, Position, RewardsResponse}; + +use crate::state::{ + get_earliest_address_lp_weight, get_incentives_by_lp_asset, get_latest_address_lp_weight, + get_open_positions_by_receiver, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, INCENTIVES, + LAST_CLAIMED_EPOCH, LP_WEIGHTS_HISTORY, +}; use crate::ContractError; +//todo maybe make it claim rewards PER position, or at least per lp_denom, the way it is now it can be computationally expensive /// Claims pending rewards for incentives where the user has LP -pub(crate) fn claim( - deps: DepsMut, - _env: Env, - info: MessageInfo, -) -> Result { +pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result { cw_utils::nonpayable(&info)?; // check if the user has any open LP positions - let open_positions = get_open_positions_by_receiver(deps.storage, info.sender.into_string())?; - - if open_positions.is_empty() { - return Err(ContractError::NoOpenPositions); - } + let open_positions = + get_open_positions_by_receiver(deps.storage, info.sender.clone().into_string())?; + ensure!(!open_positions.is_empty(), ContractError::NoOpenPositions); let config = CONFIG.load(deps.storage)?; - let _current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( + let current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( deps.as_ref(), config.epoch_manager_addr.into_string(), )?; - //todo complete this + let mut total_rewards = vec![]; + + for position in open_positions { + // calculate the rewards for the position + let rewards_response = calculate_rewards(deps.as_ref(), position, current_epoch.id, true)?; + + match rewards_response { + RewardsResponse::ClaimRewards { + rewards, + modified_incentives, + } => { + total_rewards.append(&mut rewards.clone()); + + // update the incentives with the claimed rewards + for (incentive_identifier, claimed_reward) in modified_incentives { + INCENTIVES.update( + deps.storage, + &incentive_identifier, + |incentive| -> Result<_, ContractError> { + let mut incentive = incentive.unwrap(); + incentive.claimed_amount = + incentive.claimed_amount.checked_add(claimed_reward)?; + Ok(incentive) + }, + )?; + } + } + _ => return Err(ContractError::Unauthorized), + } + } + + // update the last claimed epoch for the user + LAST_CLAIMED_EPOCH.save(deps.storage, &info.sender, ¤t_epoch.id)?; + + // sync the address lp weight history for the user + sync_address_lp_weight_history(deps.storage, &info.sender, ¤t_epoch.id)?; + + Ok(Response::default() + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: aggregate_coins(total_rewards)?, + })) + .add_attributes(vec![("action", "claim".to_string())])) +} + +/// Calculates the rewards for a position +/// ### Returns +/// A [RewardsResponse] with the rewards for the position. If is_claim is true, the RewardsResponse type is +/// ClaimRewards, which contains the rewards and the modified incentives (this is to modify the +/// incentives in the claim function afterwards). If is_claim is false, the RewardsResponse only returns +/// the rewards. +pub(crate) fn calculate_rewards( + deps: Deps, + position: Position, + current_epoch_id: EpochId, + is_claim: bool, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let incentives = get_incentives_by_lp_asset( + deps.storage, + &position.lp_asset.denom, + None, + Some(config.max_concurrent_incentives), + )?; + + let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, &position.receiver)?; + + // Check if the user ever claimed before + if let Some(last_claimed_epoch) = last_claimed_epoch { + // if the last claimed epoch is the same as the current epoch, then there is nothing to claim + if current_epoch_id == last_claimed_epoch { + return if is_claim { + Ok(RewardsResponse::ClaimRewards { + rewards: vec![], + modified_incentives: Default::default(), + }) + } else { + Ok(RewardsResponse::RewardsResponse { rewards: vec![] }) + }; + } + } + + let mut rewards: Vec = vec![]; + let mut modified_incentives: HashMap = HashMap::new(); + + for incentive in incentives { + if incentive.is_expired(current_epoch_id) { + continue; + } + + // compute where the user can start claiming rewards for the incentive + let start_from_epoch = compute_start_from_epoch_for_incentive( + deps.storage, + &incentive, + last_claimed_epoch, + &position.receiver, + )?; + + // compute the weights of the user for the epochs between start_from_epoch and current_epoch_id + let user_weights = compute_user_weights( + deps.storage, + &position.receiver, + &start_from_epoch, + ¤t_epoch_id, + )?; + + // compute the incentive emissions for the epochs between start_from_epoch and current_epoch_id + let (incentive_emissions, until_epoch) = + compute_incentive_emissions(&incentive, &start_from_epoch, ¤t_epoch_id)?; + + for epoch_id in start_from_epoch..=until_epoch { + let user_weight = user_weights[&epoch_id]; + let total_lp_weight = + LP_WEIGHTS_HISTORY.load(deps.storage, (&position.lp_asset.denom, epoch_id))?; + + let user_share = (user_weight, total_lp_weight); + + let reward = incentive_emissions + .get(&epoch_id) + .unwrap_or(&Uint128::zero()) + .to_owned() + .checked_mul_floor(user_share)?; + + // sanity check + ensure!( + reward.checked_add(incentive.claimed_amount)? <= incentive.incentive_asset.amount, + ContractError::IncentiveExhausted + ); + + rewards.push(Coin { + denom: incentive.incentive_asset.denom.clone(), + amount: reward, + }); + + if is_claim { + modified_incentives.insert(incentive.identifier.clone(), reward); + } + } + } + + rewards = aggregate_coins(rewards)?; + + // todo modify incentives, i.e. incentive.claimed_amount + + if is_claim { + Ok(RewardsResponse::ClaimRewards { + rewards, + modified_incentives, + }) + } else { + Ok(RewardsResponse::RewardsResponse { rewards }) + } +} + +/// Computes the epoch from which the user can start claiming rewards for a given incentive +pub(crate) fn compute_start_from_epoch_for_incentive( + storage: &dyn Storage, + incentive: &Incentive, + last_claimed_epoch: Option, + receiver: &Addr, +) -> Result { + let first_claimable_epoch_for_user = if let Some(last_claimed_epoch) = last_claimed_epoch { + // if the user has claimed before, then the next epoch is the one after the last claimed epoch + last_claimed_epoch + 1u64 + } else { + // if the user has never claimed before but has a weight, get the epoch at which the user + // first had a weight in the system + get_earliest_address_lp_weight(storage, receiver)?.0 + }; + + // returns the latest epoch between the first claimable epoch for the user and the start epoch + // of the incentive, i.e. either when the incentive starts IF the incentive starts after the + // first claimable epoch for the user, or the first claimable epoch for the user IF the incentive + // started before the user had a weight in the system + Ok(incentive.start_epoch.max(first_claimable_epoch_for_user)) +} + +/// Computes the user weights for a given LP asset. This assumes that [compute_start_from_epoch_for_incentive] +/// was called before this function, computing the start_from_epoch for the user with either the last_claimed_epoch +/// or the first epoch the user had a weight in the system. +pub(crate) fn compute_user_weights( + storage: &dyn Storage, + receiver: &Addr, + start_from_epoch: &EpochId, + current_epoch_id: &EpochId, +) -> Result, ContractError> { + let mut user_weights = HashMap::new(); + + let mut last_weight_seen = Uint128::zero(); + + // starts from start_from_epoch - 1 in case the user has a last_claimed_epoch, which means the user + // has a weight for the last_claimed_epoch. [compute_start_from_epoch_for_incentive] would return + // last_claimed_epoch + 1 in that case, which is correct, and if the user has not modified its + // position, the weight will be the same for start_from_epoch as it is for last_claimed_epoch. + for epoch_id in *start_from_epoch - 1..=*current_epoch_id { + let weight = ADDRESS_LP_WEIGHT_HISTORY.may_load(storage, (receiver, epoch_id))?; + if let Some(weight) = weight { + last_weight_seen = weight; + user_weights.insert(epoch_id, weight); + } else { + user_weights.insert(epoch_id, last_weight_seen); + } + } + + Ok(user_weights) +} + +/// Computes the incentive emissions for a given incentive. Let's assume for now that the incentive +/// is expanded by a multiple of the original emission rate. todo revise this +/// ### Returns +/// A pair with the incentive emissions for each epoch between start_from_epoch and the current_epoch_id in a hashmap +/// and the last epoch for which the incentive emissions were computed +fn compute_incentive_emissions( + incentive: &Incentive, + start_from_epoch: &EpochId, + current_epoch_id: &EpochId, +) -> Result<(HashMap, EpochId), ContractError> { + let mut incentive_emissions = HashMap::new(); + + let until_epoch = if incentive.preliminary_end_epoch < *current_epoch_id { + incentive.preliminary_end_epoch + } else { + *current_epoch_id + }; + + for epoch in *start_from_epoch..=until_epoch { + incentive_emissions.insert(epoch, incentive.emission_rate); + } + + Ok((incentive_emissions, until_epoch)) +} + +/// Syncs the address lp weight history for the given address and epoch_id, removing all the previous +/// entries as the user has already claimed those epochs, and setting the weight for the current epoch. +fn sync_address_lp_weight_history( + storage: &mut dyn Storage, + address: &Addr, + current_epoch_id: &u64, +) -> Result<(), ContractError> { + let (earliest_epoch_id, _) = get_earliest_address_lp_weight(storage, address)?; + let (latest_epoch_id, latest_address_lp_weight) = + get_latest_address_lp_weight(storage, address)?; + + // remove previous entries + for epoch_id in earliest_epoch_id..=latest_epoch_id { + ADDRESS_LP_WEIGHT_HISTORY.remove(storage, (address, epoch_id)); + } + + // save the latest weight for the current epoch + ADDRESS_LP_WEIGHT_HISTORY.save( + storage, + (address, *current_epoch_id), + &latest_address_lp_weight, + )?; - Ok(Response::default().add_attributes(vec![("action", "claim".to_string())])) + Ok(()) } diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs index 5d3399396..d1d037170 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs @@ -1,2 +1,5 @@ pub mod commands; mod queries; + +#[cfg(test)] +mod tests; diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs new file mode 100644 index 000000000..2e7a7f241 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs @@ -0,0 +1,11 @@ +use crate::state::ADDRESS_LP_WEIGHT_HISTORY; +use cosmwasm_std::{Addr, StdResult, Storage, Uint128}; + +pub(crate) fn fill_address_lp_weight_history( + storage: &mut dyn Storage, + address: &Addr, + epoch_id: u64, + weight: Uint128, +) -> StdResult<()> { + ADDRESS_LP_WEIGHT_HISTORY.save(storage, (address, epoch_id), &weight) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs new file mode 100644 index 000000000..699f16489 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs @@ -0,0 +1,2 @@ +mod helpers; +mod rewards; diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs new file mode 100644 index 000000000..e3bbcf13f --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs @@ -0,0 +1,161 @@ +use crate::incentive::commands::{compute_start_from_epoch_for_incentive, compute_user_weights}; +use crate::state::ADDRESS_LP_WEIGHT_HISTORY; +use cosmwasm_std::{Addr, Coin, Storage, Uint128}; +use white_whale_std::incentive_manager::{Curve, EpochId, Incentive}; +use white_whale_std::pool_network::asset::{Asset, AssetInfo}; +use white_whale_std::pool_network::mock_querier::mock_dependencies; + +#[test] +fn compute_start_from_epoch_for_incentive_successfully() { + let mut deps = mock_dependencies(&[]); + let user = Addr::unchecked("user"); + + let mut incentive = Incentive { + identifier: "incentive".to_string(), + owner: user.clone(), + lp_denom: "lp".to_string(), + incentive_asset: Coin { + denom: "incentive".to_string(), + amount: Uint128::new(1_000), + }, + claimed_amount: Default::default(), + emission_rate: Default::default(), + curve: Curve::Linear, + start_epoch: 10, + preliminary_end_epoch: 20, + last_epoch_claimed: 9, + }; + + let current_epoch_id = 12u64; + + // Mimics the scenario where the user has never claimed before, but opened a position before the incentive + // went live + let first_user_weight_epoch_id = 8; + ADDRESS_LP_WEIGHT_HISTORY + .save( + &mut deps.storage, + (&user, first_user_weight_epoch_id), + &Uint128::one(), + ) + .unwrap(); + + let start_from_epoch = + compute_start_from_epoch_for_incentive(&deps.storage, &incentive, None, &user).unwrap(); + + // the function should return the start epoch of the incentive + assert_eq!(start_from_epoch, 10); + + // Mimics the scenario where the user has never claimed before, but opened a position after the incentive + // went live + incentive.start_epoch = 5u64; + let start_from_epoch = + compute_start_from_epoch_for_incentive(&deps.storage, &incentive, None, &user).unwrap(); + + // the function should return the first epoch the user has a weight + assert_eq!(start_from_epoch, 8); + + // Mimics the scenario where the user has claimed already, after the incentive went live, i.e. the user + // has already partially claimed this incentive + incentive.start_epoch = 10u64; + let start_from_epoch = + compute_start_from_epoch_for_incentive(&deps.storage, &incentive, Some(12u64), &user) + .unwrap(); + + // the function should return the next epoch after the last claimed one + assert_eq!(start_from_epoch, 13); + + // Mimics the scenario where the user has claimed already, before the incentive went live, i.e. the user + // has not claimed this incentive at all + incentive.start_epoch = 15u64; + let start_from_epoch = + compute_start_from_epoch_for_incentive(&deps.storage, &incentive, Some(12u64), &user) + .unwrap(); + + // the function should return the start epoch of the incentive + assert_eq!(start_from_epoch, 15); + + // Mimics the scenario where the user has claimed the epoch the incentives went live + incentive.start_epoch = 15u64; + let start_from_epoch = + compute_start_from_epoch_for_incentive(&deps.storage, &incentive, Some(15u64), &user) + .unwrap(); + + // the function should return the next epoch after the last claimed one + assert_eq!(start_from_epoch, 16); +} + +#[test] +fn compute_user_weights_successfully() { + let mut deps = mock_dependencies(&[]); + + let user = Addr::unchecked("user"); + + let mut start_from_epoch = 1u64; + let mut current_epoch_id = 10u64; + + // fill the lp_weight_history for the address with + // [(1,2), (2,4), (3,6), (4,8), (5,10), (6,12), (7,14), (8,16), (9,18), (10,20)] + for epoch in 1u64..=10u64 { + let weight = Uint128::new(epoch as u128 * 2u128); + ADDRESS_LP_WEIGHT_HISTORY + .save(&mut deps.storage, (&user, epoch), &weight) + .unwrap(); + } + + let weights = + compute_user_weights(&deps.storage, &user, &start_from_epoch, ¤t_epoch_id).unwrap(); + assert_eq!(weights.len(), 11); + + for epoch in 1u64..=10u64 { + assert_eq!( + weights.get(&epoch).unwrap(), + &Uint128::new(epoch as u128 * 2u128) + ); + + // reset the weight for epochs + ADDRESS_LP_WEIGHT_HISTORY.remove(&mut deps.storage, (&user, epoch)); + } + + // fill the lp_weight_history for the address with + // [(1,2), (5,10), (7,14)] + for epoch in 1u64..=10u64 { + if epoch % 2 == 0 || epoch % 3 == 0 { + continue; + } + + let weight = Uint128::new(epoch as u128 * 2u128); + ADDRESS_LP_WEIGHT_HISTORY + .save(&mut deps.storage, (&user, epoch), &weight) + .unwrap(); + } + + // The result should be [(1,2), (5,10), (10,14)], with the skipped valued in between having the same + // value as the previous, most recent value, i.e. epoch 2 3 4 having the value of 1 (latest weight seen in epoch 1) + // then 5..7 having the value of 10 (latest weight seen in epoch 5) + // then 8..=10 having the value of 14 (latest weight seen in epoch 7) + let weights = + compute_user_weights(&deps.storage, &user, &start_from_epoch, ¤t_epoch_id).unwrap(); + assert_eq!(weights.len(), 11); + + assert_eq!(weights.get(&1).unwrap(), &Uint128::new(2)); + assert_eq!(weights.get(&4).unwrap(), &Uint128::new(2)); + assert_eq!(weights.get(&5).unwrap(), &Uint128::new(10)); + assert_eq!(weights.get(&6).unwrap(), &Uint128::new(10)); + assert_eq!(weights.get(&7).unwrap(), &Uint128::new(14)); + assert_eq!(weights.get(&10).unwrap(), &Uint128::new(14)); + + start_from_epoch = 6u64; + let weights = + compute_user_weights(&deps.storage, &user, &start_from_epoch, ¤t_epoch_id).unwrap(); + assert_eq!(weights.len(), 6); + + assert_eq!(weights.get(&5).unwrap(), &Uint128::new(10)); + assert_eq!(weights.get(&6).unwrap(), &Uint128::new(10)); + assert_eq!(weights.get(&7).unwrap(), &Uint128::new(14)); + assert_eq!(weights.get(&10).unwrap(), &Uint128::new(14)); + + for epoch in 1u64..=10u64 { + // reset the weight for epochs + ADDRESS_LP_WEIGHT_HISTORY.remove(&mut deps.storage, (&user, epoch)); + } +} diff --git a/contracts/liquidity_hub/incentive-manager/src/position/queries.rs b/contracts/liquidity_hub/incentive-manager/src/position/queries.rs index ad3ea1e20..f69065fc4 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/queries.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/queries.rs @@ -14,11 +14,11 @@ pub(crate) fn _get_rewards(deps: Deps, address: Addr) -> Result Result<(EpochId, Uint128), ContractError> { + let earliest_weight_history_result = ADDRESS_LP_WEIGHT_HISTORY + .prefix(address) + .range(storage, None, None, Order::Ascending) + .next() + .transpose(); + + match earliest_weight_history_result { + Ok(Some(item)) => Ok(item), + Ok(None) => Err(ContractError::NoOpenPositions), + Err(std_err) => Err(std_err.into()), + } +} + +/// Gets the latest entry of an address in the address lp weight history. +/// If the address has no open positions, it returns an error. +pub fn get_latest_address_lp_weight( + storage: &dyn Storage, + address: &Addr, +) -> Result<(EpochId, Uint128), ContractError> { + let latest_weight_history_result = ADDRESS_LP_WEIGHT_HISTORY + .prefix(address) + .range(storage, None, None, Order::Descending) + .next() + .transpose(); + + match latest_weight_history_result { + Ok(Some(item)) => Ok(item), + Ok(None) => Err(ContractError::NoOpenPositions), + Err(std_err) => Err(std_err.into()), + } +} diff --git a/packages/white-whale-std/src/coin.rs b/packages/white-whale-std/src/coin.rs index 466edd7c8..7a89b5e72 100644 --- a/packages/white-whale-std/src/coin.rs +++ b/packages/white-whale-std/src/coin.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{StdError, StdResult}; +use std::collections::HashMap; + +use cosmwasm_std::{Coin, StdError, StdResult, Uint128}; #[cfg(feature = "injective")] pub const PEGGY_PREFIX: &str = "peggy"; @@ -132,3 +134,25 @@ fn get_factory_token_label(denom: &str) -> StdResult { } //todo test these functions in isolation + +/// Aggregates coins from two vectors, summing up the amounts of coins that are the same. +pub fn aggregate_coins(coins: Vec) -> StdResult> { + let mut aggregation_map: HashMap = HashMap::new(); + + // aggregate coins by denom + for coin in coins { + if let Some(existing_amount) = aggregation_map.get_mut(&coin.denom) { + *existing_amount = existing_amount.checked_add(coin.amount)?; + } else { + aggregation_map.insert(coin.denom.clone(), coin.amount); + } + } + + // create a new vector from the aggregation map + let mut aggregated_coins: Vec = Vec::new(); + for (denom, amount) in aggregation_map { + aggregated_coins.push(Coin { denom, amount }); + } + + Ok(aggregated_coins) +} diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index b611e8ec9..2f6a7f6a5 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; +use std::collections::HashMap; use crate::epoch_manager::hooks::EpochChangedHookMsg; @@ -232,11 +233,18 @@ pub struct Position { /// The owner of the position. pub receiver: Addr, } - #[cw_serde] -pub struct RewardsResponse { - /// The rewards that is available to a user if they executed the `claim` function at this point. - pub rewards: Vec, +pub enum RewardsResponse { + RewardsResponse { + /// The rewards that is available to a user if they executed the `claim` function at this point. + rewards: Vec, + }, + ClaimRewards { + /// The rewards that is available to a user if they executed the `claim` function at this point. + rewards: Vec, + /// The rewards that were claimed on each incentive, if any. + modified_incentives: HashMap, + }, } /// Minimum amount of an asset to create an incentive with From 2be651b317da66e3e82a2421ab2eb11137797c05 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 2 Apr 2024 12:24:52 +0100 Subject: [PATCH 17/35] chore(incentive): ensure expansion amount is a multiple of the emoriginal ission rate --- contracts/liquidity_hub/incentive-manager/src/error.rs | 6 ++++++ .../incentive-manager/src/manager/commands.rs | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index ecdca7250..98844f3ff 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -156,6 +156,12 @@ pub enum ContractError { #[error("There are pending rewards to be claimed before this action can be executed")] PendingRewards, + + #[error("The incentive expansion amount must be a multiple of the emission rate, which is {emission_rate}")] + InvalidExpansionAmount { + /// The emission rate of the incentive + emission_rate: Uint128, + }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index e9998b78b..54f618b3c 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -254,6 +254,14 @@ fn expand_incentive( ContractError::AssetMismatch ); + // make sure the expansion is a multiple of the emission rate + ensure!( + params.incentive_asset.amount % incentive.emission_rate == Uint128::zero(), + ContractError::InvalidExpansionAmount { + emission_rate: incentive.emission_rate + } + ); + // increase the total amount of the incentive incentive.incentive_asset.amount = incentive .incentive_asset From 137f3262b0fe2b615785225d9a3956dabbbd62ad Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 2 Apr 2024 12:25:31 +0100 Subject: [PATCH 18/35] chore: add queries --- .../liquidity_hub/incentive-manager/src/contract.rs | 13 +++++++++---- .../liquidity_hub/incentive-manager/src/lib.rs | 1 + .../liquidity_hub/incentive-manager/src/queries.rs | 9 +++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/src/queries.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 8745a36e8..6fa48c0d1 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, +}; use cw2::{get_contract_version, set_contract_version}; use semver::Version; @@ -11,7 +13,7 @@ use crate::error::ContractError; use crate::helpers::validate_emergency_unlock_penalty; use crate::position::commands::{close_position, fill_position, withdraw_position}; use crate::state::CONFIG; -use crate::{incentive, manager}; +use crate::{incentive, manager, queries}; const CONTRACT_NAME: &str = "white-whale_incentive-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -148,8 +150,11 @@ pub fn execute( } #[entry_point] -pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { - unimplemented!() +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Config {} => Ok(to_json_binary(&queries::query_manager_config(deps)?)?), + QueryMsg::Ownership {} => Ok(to_json_binary(&cw_ownable::get_ownership(deps.storage)?)?), + } } #[cfg(not(tarpaulin_include))] diff --git a/contracts/liquidity_hub/incentive-manager/src/lib.rs b/contracts/liquidity_hub/incentive-manager/src/lib.rs index 3f4c88cc1..b3b6cb40e 100644 --- a/contracts/liquidity_hub/incentive-manager/src/lib.rs +++ b/contracts/liquidity_hub/incentive-manager/src/lib.rs @@ -4,6 +4,7 @@ pub mod helpers; pub mod incentive; mod manager; pub mod position; +mod queries; pub mod state; pub use crate::error::ContractError; diff --git a/contracts/liquidity_hub/incentive-manager/src/queries.rs b/contracts/liquidity_hub/incentive-manager/src/queries.rs new file mode 100644 index 000000000..4c4772cea --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/queries.rs @@ -0,0 +1,9 @@ +use crate::state::CONFIG; +use crate::ContractError; +use cosmwasm_std::Deps; +use white_whale_std::incentive_manager::Config; + +/// Queries the manager config +pub(crate) fn query_manager_config(deps: Deps) -> Result { + Ok(CONFIG.load(deps.storage)?) +} From 8aa8a9d6eb3ef2e3f0fc051366a7c04888a0ccc4 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 2 Apr 2024 15:09:13 +0100 Subject: [PATCH 19/35] chore: add remaining queries --- .../incentive-manager/src/contract.rs | 21 ++++- .../src/incentive/commands.rs | 12 +-- .../incentive-manager/src/incentive/mod.rs | 2 - .../src/incentive/queries.rs | 1 - .../incentive-manager/src/manager/commands.rs | 4 +- .../incentive-manager/src/position/mod.rs | 1 - .../incentive-manager/src/position/queries.rs | 24 ----- .../incentive-manager/src/queries.rs | 93 ++++++++++++++++++- .../incentive-manager/src/state.rs | 43 +++++---- .../white-whale-std/src/incentive_manager.rs | 50 +++++++++- 10 files changed, 189 insertions(+), 62 deletions(-) delete mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/queries.rs delete mode 100644 contracts/liquidity_hub/incentive-manager/src/position/queries.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 6fa48c0d1..78c137026 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -152,8 +152,27 @@ pub fn execute( #[entry_point] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::Config {} => Ok(to_json_binary(&queries::query_manager_config(deps)?)?), + QueryMsg::Config => Ok(to_json_binary(&queries::query_manager_config(deps)?)?), QueryMsg::Ownership {} => Ok(to_json_binary(&cw_ownable::get_ownership(deps.storage)?)?), + QueryMsg::Incentives { + filter_by, + start_after, + limit, + } => Ok(to_json_binary(&queries::query_incentives( + deps, + filter_by, + start_after, + limit, + )?)?), + QueryMsg::Positions { + address, + open_state, + } => Ok(to_json_binary(&queries::query_positions( + deps, address, open_state, + )?)?), + QueryMsg::Rewards { address } => { + Ok(to_json_binary(&queries::query_rewards(deps, address)?)?) + } } } diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index cf2fc2a1f..7a87eec06 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -8,9 +8,9 @@ use white_whale_std::coin::aggregate_coins; use white_whale_std::incentive_manager::{EpochId, Incentive, Position, RewardsResponse}; use crate::state::{ - get_earliest_address_lp_weight, get_incentives_by_lp_asset, get_latest_address_lp_weight, - get_open_positions_by_receiver, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, INCENTIVES, - LAST_CLAIMED_EPOCH, LP_WEIGHTS_HISTORY, + get_earliest_address_lp_weight, get_incentives_by_lp_denom, get_latest_address_lp_weight, + get_positions_by_receiver, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, INCENTIVES, LAST_CLAIMED_EPOCH, + LP_WEIGHTS_HISTORY, }; use crate::ContractError; @@ -21,7 +21,7 @@ pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result Result { let config = CONFIG.load(deps.storage)?; - let incentives = get_incentives_by_lp_asset( + let incentives = get_incentives_by_lp_denom( deps.storage, &position.lp_asset.denom, None, @@ -173,8 +173,6 @@ pub(crate) fn calculate_rewards( rewards = aggregate_coins(rewards)?; - // todo modify incentives, i.e. incentive.claimed_amount - if is_claim { Ok(RewardsResponse::ClaimRewards { rewards, diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs index d1d037170..1225d166b 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/mod.rs @@ -1,5 +1,3 @@ pub mod commands; -mod queries; - #[cfg(test)] mod tests; diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/queries.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/queries.rs deleted file mode 100644 index 8b1378917..000000000 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/queries.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 54f618b3c..4ba69f448 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -14,7 +14,7 @@ use crate::helpers::{ validate_incentive_epochs, }; use crate::state::{ - get_incentive_by_identifier, get_incentives_by_lp_asset, CONFIG, INCENTIVES, INCENTIVE_COUNTER, + get_incentive_by_identifier, get_incentives_by_lp_denom, CONFIG, INCENTIVES, INCENTIVE_COUNTER, LP_WEIGHTS_HISTORY, }; use crate::ContractError; @@ -50,7 +50,7 @@ fn create_incentive( ) -> Result { // check if there are any expired incentives for this LP asset let config = CONFIG.load(deps.storage)?; - let incentives = get_incentives_by_lp_asset( + let incentives = get_incentives_by_lp_denom( deps.storage, ¶ms.lp_denom, None, diff --git a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs index 718bed737..39da526a6 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs @@ -1,3 +1,2 @@ pub mod commands; mod helpers; -mod queries; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/queries.rs b/contracts/liquidity_hub/incentive-manager/src/position/queries.rs deleted file mode 100644 index f69065fc4..000000000 --- a/contracts/liquidity_hub/incentive-manager/src/position/queries.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::state::{CONFIG, LAST_CLAIMED_EPOCH}; -use crate::ContractError; -use cosmwasm_std::{Addr, Deps}; -use white_whale_std::epoch_manager::common::get_current_epoch; -use white_whale_std::incentive_manager::RewardsResponse; - -pub(crate) fn _get_rewards(deps: Deps, address: Addr) -> Result { - let config = CONFIG.load(deps.storage)?; - let current_epoch = get_current_epoch(deps, config.epoch_manager_addr.into_string())?; - - let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, &address)?; - - // Check if the user ever claimed before - if let Some(last_claimed_epoch) = last_claimed_epoch { - // if the last claimed epoch is the same as the current epoch, then there is nothing to claim - if current_epoch.id == last_claimed_epoch { - return Ok(RewardsResponse::RewardsResponse { rewards: vec![] }); - } - } - - let rewards = vec![]; - - Ok(RewardsResponse::RewardsResponse { rewards }) -} diff --git a/contracts/liquidity_hub/incentive-manager/src/queries.rs b/contracts/liquidity_hub/incentive-manager/src/queries.rs index 4c4772cea..dd267b4ee 100644 --- a/contracts/liquidity_hub/incentive-manager/src/queries.rs +++ b/contracts/liquidity_hub/incentive-manager/src/queries.rs @@ -1,9 +1,96 @@ -use crate::state::CONFIG; -use crate::ContractError; use cosmwasm_std::Deps; -use white_whale_std::incentive_manager::Config; +use white_whale_std::coin::aggregate_coins; + +use white_whale_std::incentive_manager::{ + Config, IncentivesBy, IncentivesResponse, PositionsResponse, RewardsResponse, +}; + +use crate::incentive::commands::calculate_rewards; +use crate::state::{ + get_incentive_by_identifier, get_incentives, get_incentives_by_incentive_asset, + get_incentives_by_lp_denom, get_positions_by_receiver, CONFIG, +}; +use crate::ContractError; /// Queries the manager config pub(crate) fn query_manager_config(deps: Deps) -> Result { Ok(CONFIG.load(deps.storage)?) } + +/// Queries all incentives. If `lp_asset` is provided, it will return all incentives for that +/// particular lp. +pub(crate) fn query_incentives( + deps: Deps, + filter_by: Option, + start_after: Option, + limit: Option, +) -> Result { + let incentives = if let Some(filter_by) = filter_by { + match filter_by { + IncentivesBy::Identifier(identifier) => { + vec![get_incentive_by_identifier(deps.storage, &identifier)?] + } + IncentivesBy::LPDenom(lp_denom) => { + get_incentives_by_lp_denom(deps.storage, lp_denom.as_str(), start_after, limit)? + } + IncentivesBy::IncentiveAsset(incentive_asset) => get_incentives_by_incentive_asset( + deps.storage, + incentive_asset.as_str(), + start_after, + limit, + )?, + } + } else { + get_incentives(deps.storage, start_after, limit)? + }; + + Ok(IncentivesResponse { incentives }) +} + +/// Queries all positions. If `open_state` is provided, it will return all positions that match that +/// open state, i.e. open positions if true, closed positions if false. +pub(crate) fn query_positions( + deps: Deps, + address: String, + open_state: Option, +) -> Result { + let positions = get_positions_by_receiver(deps.storage, address, open_state)?; + + Ok(PositionsResponse { positions }) +} + +/// Queries the rewards for a given address. +pub(crate) fn query_rewards(deps: Deps, address: String) -> Result { + let receiver = deps.api.addr_validate(&address)?; + // check if the user has any open LP positions + let open_positions = + get_positions_by_receiver(deps.storage, receiver.into_string(), Some(true))?; + + if open_positions.is_empty() { + // if the user has no open LP positions, return an empty rewards list + return Ok(RewardsResponse::RewardsResponse { rewards: vec![] }); + } + + let config = CONFIG.load(deps.storage)?; + let current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( + deps, + config.epoch_manager_addr.into_string(), + )?; + + let mut total_rewards = vec![]; + + for position in open_positions { + // calculate the rewards for the position + let rewards_response = calculate_rewards(deps, position, current_epoch.id, false)?; + match rewards_response { + RewardsResponse::RewardsResponse { rewards } => { + total_rewards.append(&mut rewards.clone()) + } + _ => return Err(ContractError::Unauthorized), + } + } + + Ok(RewardsResponse::RewardsResponse { + rewards: aggregate_coins(total_rewards)?, + }) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 4bd010774..c1d1b808d 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -4,7 +4,6 @@ use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use white_whale_std::incentive_manager::{Config, EpochId, Incentive, Position}; -use white_whale_std::pool_network::asset::AssetInfo; use crate::ContractError; @@ -63,7 +62,7 @@ pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); pub const INCENTIVES: IndexedMap<&str, Incentive, IncentiveIndexes> = IndexedMap::new( "incentives", IncentiveIndexes { - lp_asset: MultiIndex::new( + lp_denom: MultiIndex::new( |_pk, i| i.lp_denom.to_string(), "incentives", "incentives__lp_asset", @@ -77,13 +76,13 @@ pub const INCENTIVES: IndexedMap<&str, Incentive, IncentiveIndexes> = IndexedMap ); pub struct IncentiveIndexes<'a> { - pub lp_asset: MultiIndex<'a, String, Incentive, String>, + pub lp_denom: MultiIndex<'a, String, Incentive, String>, pub incentive_asset: MultiIndex<'a, String, Incentive, String>, } impl<'a> IndexList for IncentiveIndexes<'a> { fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.lp_asset, &self.incentive_asset]; + let v: Vec<&dyn Index> = vec![&self.lp_denom, &self.incentive_asset]; Box::new(v.into_iter()) } } @@ -112,8 +111,8 @@ pub fn get_incentives( .collect() } -/// Gets incentives given an lp asset [AssetInfo] -pub fn get_incentives_by_lp_asset( +/// Gets incentives given an lp denom. +pub fn get_incentives_by_lp_denom( storage: &dyn Storage, lp_denom: &str, start_after: Option, @@ -124,7 +123,7 @@ pub fn get_incentives_by_lp_asset( INCENTIVES .idx - .lp_asset + .lp_denom .prefix(lp_denom.to_owned()) .range(storage, start, None, Order::Ascending) .take(limit) @@ -136,10 +135,10 @@ pub fn get_incentives_by_lp_asset( .collect() } -/// Gets incentives given an incentive asset as [AssetInfo] -pub fn get_incentive_by_asset( +/// Gets all the incentives that are offering the given incentive_asset as a reward. +pub fn get_incentives_by_incentive_asset( storage: &dyn Storage, - incentive_asset: &AssetInfo, + incentive_asset: &str, start_after: Option, limit: Option, ) -> StdResult> { @@ -149,7 +148,7 @@ pub fn get_incentive_by_asset( INCENTIVES .idx .incentive_asset - .prefix(incentive_asset.to_string()) + .prefix(incentive_asset.to_owned()) .range(storage, start, None, Order::Ascending) .take(limit) .map(|item| { @@ -184,15 +183,15 @@ pub fn get_position( } } -//todo think of the limit when claiming rewards -/// Gets the positions of the given receiver. -pub fn get_open_positions_by_receiver( +/// Gets all the positions of the given receiver. +pub fn get_positions_by_receiver( storage: &dyn Storage, receiver: String, + open_state: Option, ) -> StdResult> { let limit = MAX_LIMIT as usize; - let open_positions = POSITIONS + let mut positions_by_receiver = POSITIONS .idx .receiver .prefix(receiver) @@ -202,12 +201,16 @@ pub fn get_open_positions_by_receiver( let (_, position) = item?; Ok(position) }) - .collect::>>()? - .into_iter() - .filter(|position| position.open) - .collect::>(); + .collect::>>()?; + + if let Some(open) = open_state { + positions_by_receiver = positions_by_receiver + .into_iter() + .filter(|position| position.open == open) + .collect::>(); + } - Ok(open_positions) + Ok(positions_by_receiver) } /// Gets the earliest entry of an address in the address lp weight history. diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index 2f6a7f6a5..100033ae2 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -76,7 +76,42 @@ pub struct MigrateMsg {} pub enum QueryMsg { /// Retrieves the configuration of the manager. #[returns(Config)] - Config {}, + Config, + /// Retrieves the configuration of the manager. + #[returns(IncentivesResponse)] + Incentives { + /// An optional parameter specifying what to filter incentives by. + /// Can be either the incentive identifier, lp denom or the incentive asset. + filter_by: Option, + /// An optional parameter specifying what incentive (identifier) to start searching after. + start_after: Option, + /// The amount of incentives to return. + /// If unspecified, will default to a value specified by the contract. + limit: Option, + }, + /// Retrieves the positions for an address. + #[returns(PositionsResponse)] + Positions { + /// The address to get positions for. + address: String, + /// An optional parameter specifying to return only positions that match the given open state. + /// if true, it will return open positions. If false, it will return closed positions. + open_state: Option, + }, + /// Retrieves the rewards for an address. + #[returns(RewardsResponse)] + Rewards { + /// The address to get all the incentive rewards for. + address: String, + }, +} + +/// Enum to filter incentives by identifier, lp denom or the incentive asset. Used in the Incentives query. +#[cw_serde] +pub enum IncentivesBy { + Identifier(String), + LPDenom(String), + IncentiveAsset(String), } /// Configuration for the contract (manager) @@ -252,3 +287,16 @@ pub const MIN_INCENTIVE_AMOUNT: Uint128 = Uint128::new(1_000u128); /// Default incentive duration in epochs pub const DEFAULT_INCENTIVE_DURATION: u64 = 14u64; + +/// The response for the incentives query +#[cw_serde] +pub struct IncentivesResponse { + /// The list of incentives + pub incentives: Vec, +} + +#[cw_serde] +pub struct PositionsResponse { + /// All the positions a user has. + pub positions: Vec, +} From 734b6f71f5ad7e8e1cd28954d54bb073591687b1 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 3 Apr 2024 10:23:49 +0100 Subject: [PATCH 20/35] chore: remove unnecessary positions indexes from map --- .../incentive-manager/src/contract.rs | 17 ++++++++++++----- .../incentive-manager/src/position/commands.rs | 7 ++++--- .../incentive-manager/src/state.rs | 10 +--------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 78c137026..cd73eeff3 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -11,9 +11,8 @@ use white_whale_std::vault_manager::MigrateMsg; use crate::error::ContractError; use crate::helpers::validate_emergency_unlock_penalty; -use crate::position::commands::{close_position, fill_position, withdraw_position}; use crate::state::CONFIG; -use crate::{incentive, manager, queries}; +use crate::{incentive, manager, position, queries}; const CONTRACT_NAME: &str = "white-whale_incentive-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -112,15 +111,23 @@ pub fn execute( identifier, unlocking_duration, receiver, - } => fill_position(deps, info, identifier, unlocking_duration, receiver), + } => position::commands::fill_position( + deps, + info, + identifier, + unlocking_duration, + receiver, + ), PositionAction::Close { identifier, lp_asset, - } => close_position(deps, env, info, identifier, lp_asset), + } => position::commands::close_position(deps, env, info, identifier, lp_asset), PositionAction::Withdraw { identifier, emergency_unlock, - } => withdraw_position(deps, env, info, identifier, emergency_unlock), + } => { + position::commands::withdraw_position(deps, env, info, identifier, emergency_unlock) + } }, ExecuteMsg::UpdateConfig { whale_lair_addr, diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 2fa8b54d6..3d8038e8d 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -195,9 +195,10 @@ pub(crate) fn withdraw_position( }, )?; - if position.receiver != info.sender { - return Err(ContractError::Unauthorized); - } + ensure!( + position.receiver == info.sender, + ContractError::Unauthorized + ); let mut messages: Vec = vec![]; diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index c1d1b808d..d09ce9659 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -18,29 +18,21 @@ pub const POSITION_ID_COUNTER: Item = Item::new("position_id_counter"); pub const POSITIONS: IndexedMap<&str, Position, PositionIndexes> = IndexedMap::new( "positions", PositionIndexes { - lp_asset: MultiIndex::new( - |_pk, p| p.lp_asset.to_string(), - "positions", - "positions__lp_asset", - ), receiver: MultiIndex::new( |_pk, p| p.receiver.to_string(), "positions", "positions__receiver", ), - open: MultiIndex::new(|_pk, p| p.open.to_string(), "positions", "positions__open"), }, ); pub struct PositionIndexes<'a> { - pub lp_asset: MultiIndex<'a, String, Position, String>, pub receiver: MultiIndex<'a, String, Position, String>, - pub open: MultiIndex<'a, String, Position, String>, } impl<'a> IndexList for PositionIndexes<'a> { fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.lp_asset, &self.receiver, &self.open]; + let v: Vec<&dyn Index> = vec![&self.receiver]; Box::new(v.into_iter()) } } From 050c8079bc063ee0669399d19ea50a4e18582211 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 3 Apr 2024 15:34:35 +0100 Subject: [PATCH 21/35] refactor: incentive manager cleanup --- .../liquidity_hub/epoch-manager/src/state.rs | 4 +- .../epoch-manager/tests/common.rs | 4 +- .../epoch-manager/tests/epoch.rs | 6 +- .../epoch-manager/tests/instantiate.rs | 8 +-- .../incentive-manager/Cargo.toml | 2 +- .../incentive-manager/src/contract.rs | 23 +++---- .../incentive-manager/src/helpers.rs | 60 +++++++++---------- .../src/incentive/commands.rs | 14 +++-- .../incentive-manager/src/manager/commands.rs | 30 ++++------ .../src/position/commands.rs | 34 ++++++----- .../incentive-manager/src/position/helpers.rs | 21 ++++++- .../src/epoch_manager/common.rs | 6 +- .../src/epoch_manager/epoch_manager.rs | 12 ++-- .../src/epoch_manager/hooks.rs | 4 +- 14 files changed, 122 insertions(+), 106 deletions(-) diff --git a/contracts/liquidity_hub/epoch-manager/src/state.rs b/contracts/liquidity_hub/epoch-manager/src/state.rs index 923b467b7..ec9010c39 100644 --- a/contracts/liquidity_hub/epoch-manager/src/state.rs +++ b/contracts/liquidity_hub/epoch-manager/src/state.rs @@ -1,8 +1,8 @@ use cw_controllers::{Admin, Hooks}; use cw_storage_plus::{Item, Map}; -use white_whale_std::epoch_manager::epoch_manager::{Config, EpochV2}; +use white_whale_std::epoch_manager::epoch_manager::{Config, Epoch}; pub const CONFIG: Item = Item::new("config"); pub const ADMIN: Admin = Admin::new("admin"); pub const HOOKS: Hooks = Hooks::new("hooks"); -pub const EPOCHS: Map<&[u8], EpochV2> = Map::new("epochs"); +pub const EPOCHS: Map<&[u8], Epoch> = Map::new("epochs"); diff --git a/contracts/liquidity_hub/epoch-manager/tests/common.rs b/contracts/liquidity_hub/epoch-manager/tests/common.rs index ae48d517c..7ac9721a6 100644 --- a/contracts/liquidity_hub/epoch-manager/tests/common.rs +++ b/contracts/liquidity_hub/epoch-manager/tests/common.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{DepsMut, MessageInfo, Response, Uint64}; use epoch_manager::contract::{execute, instantiate}; use epoch_manager::ContractError; use white_whale_std::epoch_manager::epoch_manager::{ - EpochConfig, EpochV2, ExecuteMsg, InstantiateMsg, + Epoch, EpochConfig, ExecuteMsg, InstantiateMsg, }; /// Mocks contract instantiation. @@ -14,7 +14,7 @@ pub(crate) fn mock_instantiation( ) -> Result { let current_time = mock_env().block.time; let msg = InstantiateMsg { - start_epoch: EpochV2 { + start_epoch: Epoch { id: 123, start_time: current_time, }, diff --git a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs index 934b73dba..00f6f04b9 100644 --- a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs +++ b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs @@ -3,7 +3,7 @@ use cosmwasm_std::testing::{mock_env, mock_info}; use epoch_manager::contract::{execute, query}; use epoch_manager::ContractError; -use white_whale_std::epoch_manager::epoch_manager::{EpochResponse, EpochV2, ExecuteMsg, QueryMsg}; +use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochResponse, ExecuteMsg, QueryMsg}; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; use white_whale_std::pool_network::mock_querier::mock_dependencies; @@ -29,7 +29,7 @@ fn create_new_epoch_successfully() { let query_res = query(deps.as_ref(), mock_env(), QueryMsg::CurrentEpoch {}).unwrap(); let epoch_response: EpochResponse = from_json(query_res).unwrap(); - let current_epoch = EpochV2 { + let current_epoch = Epoch { id: 124, start_time: next_epoch_time, }; @@ -55,7 +55,7 @@ fn create_new_epoch_successfully() { assert_eq!( epoch_response.epoch, - EpochV2 { + Epoch { id: 123, start_time: next_epoch_time.minus_nanos(86400), } diff --git a/contracts/liquidity_hub/epoch-manager/tests/instantiate.rs b/contracts/liquidity_hub/epoch-manager/tests/instantiate.rs index 922108cb3..2870911f1 100644 --- a/contracts/liquidity_hub/epoch-manager/tests/instantiate.rs +++ b/contracts/liquidity_hub/epoch-manager/tests/instantiate.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{from_json, Addr, Uint64}; use epoch_manager::contract::{instantiate, query}; use epoch_manager::ContractError; use white_whale_std::epoch_manager::epoch_manager::{ - ConfigResponse, EpochConfig, EpochV2, InstantiateMsg, QueryMsg, + ConfigResponse, Epoch, EpochConfig, InstantiateMsg, QueryMsg, }; use white_whale_std::pool_network::mock_querier::mock_dependencies; @@ -17,7 +17,7 @@ fn instantiation_successful() { let current_time = mock_env().block.time; let info = mock_info("owner", &[]); let msg = InstantiateMsg { - start_epoch: EpochV2 { + start_epoch: Epoch { id: 123, start_time: current_time, }, @@ -48,7 +48,7 @@ fn instantiation_unsuccessful() { let current_time = mock_env().block.time; let info = mock_info("owner", &[]); let msg = InstantiateMsg { - start_epoch: EpochV2 { + start_epoch: Epoch { id: 123, start_time: current_time.minus_days(1), }, @@ -65,7 +65,7 @@ fn instantiation_unsuccessful() { } let msg = InstantiateMsg { - start_epoch: EpochV2 { + start_epoch: Epoch { id: 123, start_time: current_time.plus_days(1), }, diff --git a/contracts/liquidity_hub/incentive-manager/Cargo.toml b/contracts/liquidity_hub/incentive-manager/Cargo.toml index 71f7346e7..01fc5db92 100644 --- a/contracts/liquidity_hub/incentive-manager/Cargo.toml +++ b/contracts/liquidity_hub/incentive-manager/Cargo.toml @@ -3,7 +3,7 @@ name = "incentive-manager" version = "0.1.0" authors = ["Kerber0x "] edition.workspace = true -description = "The Incentive Manager is a contract that allows to manage multiple incentives in a single contract." +description = "The Incentive Manager is a contract that allows to manage multiple pool incentives in a single contract." license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index cd73eeff3..0c108fded 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, + ensure, entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, }; use cw2::{get_contract_version, set_contract_version}; use semver::Version; @@ -27,16 +27,19 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // ensure that max_concurrent_incentives is non-zero - if msg.max_concurrent_incentives == 0 { - return Err(ContractError::UnspecifiedConcurrentIncentives); - } - - if msg.max_unlocking_duration < msg.min_unlocking_duration { - return Err(ContractError::InvalidUnbondingRange { + ensure!( + msg.max_concurrent_incentives > 0, + ContractError::UnspecifiedConcurrentIncentives + ); + + // ensure the unlocking duration range is valid + ensure!( + msg.max_unlocking_duration > msg.min_unlocking_duration, + ContractError::InvalidUnbondingRange { min: msg.min_unlocking_duration, max: msg.max_unlocking_duration, - }); - } + } + ); let config = Config { epoch_manager_addr: deps.api.addr_validate(&msg.epoch_manager_addr)?, @@ -92,7 +95,7 @@ pub fn execute( match msg { ExecuteMsg::ManageIncentive { action } => match action { IncentiveAction::Fill { params } => { - manager::commands::fill_incentive(deps, env, info, params) + manager::commands::fill_incentive(deps, info, params) } IncentiveAction::Close { incentive_identifier, diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index cae5f6cc1..efd36f871 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -1,6 +1,9 @@ use std::cmp::Ordering; -use cosmwasm_std::{ensure, BankMsg, Coin, CosmosMsg, Decimal, MessageInfo, Uint128}; +use cosmwasm_std::{ + ensure, BankMsg, Coin, CosmosMsg, Decimal, MessageInfo, OverflowError, OverflowOperation, + Uint128, +}; use white_whale_std::incentive_manager::{Config, IncentiveParams, DEFAULT_INCENTIVE_DURATION}; @@ -121,52 +124,43 @@ pub(crate) fn validate_incentive_epochs( ); // ensure the incentive is set to end in a future epoch - if current_epoch > preliminary_end_epoch { - return Err(ContractError::IncentiveEndsInPast); - } + ensure!( + preliminary_end_epoch > current_epoch, + ContractError::IncentiveEndsInPast + ); let start_epoch = params.start_epoch.unwrap_or(current_epoch); // ensure that start date is before end date - if start_epoch > preliminary_end_epoch { - return Err(ContractError::IncentiveStartTimeAfterEndTime); - } + ensure!( + start_epoch <= preliminary_end_epoch, + ContractError::IncentiveStartTimeAfterEndTime + ); // ensure that start date is set within buffer - if start_epoch > current_epoch + max_incentive_epoch_buffer { - return Err(ContractError::IncentiveStartTooFar); - } + ensure!( + start_epoch + <= current_epoch + .checked_add(max_incentive_epoch_buffer) + .ok_or(ContractError::OverflowError(OverflowError { + operation: OverflowOperation::Add, + operand1: current_epoch.to_string(), + operand2: max_incentive_epoch_buffer.to_string(), + }))?, + ContractError::IncentiveStartTooFar + ); Ok((start_epoch, preliminary_end_epoch)) } -//todo maybe move this to position helpers?? -/// Validates the `unlocking_duration` specified in the position params is within the range specified -/// in the config. -pub(crate) fn validate_unlocking_duration( - config: &Config, - unlocking_duration: u64, -) -> Result<(), ContractError> { - if unlocking_duration < config.min_unlocking_duration - || unlocking_duration > config.max_unlocking_duration - { - return Err(ContractError::InvalidUnlockingDuration { - min: config.min_unlocking_duration, - max: config.max_unlocking_duration, - specified: unlocking_duration, - }); - } - - Ok(()) -} - /// Validates the emergency unlock penalty is within the allowed range (0-100%). Returns value it's validating, i.e. the penalty. pub(crate) fn validate_emergency_unlock_penalty( emergency_unlock_penalty: Decimal, ) -> Result { - if emergency_unlock_penalty > Decimal::percent(100) { - return Err(ContractError::InvalidEmergencyUnlockPenalty); - } + ensure!( + emergency_unlock_penalty <= Decimal::percent(100), + ContractError::InvalidEmergencyUnlockPenalty + ); Ok(emergency_unlock_penalty) } diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index 7a87eec06..8ecc0ad6a 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -14,7 +14,6 @@ use crate::state::{ }; use crate::ContractError; -//todo maybe make it claim rewards PER position, or at least per lp_denom, the way it is now it can be computationally expensive /// Claims pending rewards for incentives where the user has LP pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result { cw_utils::nonpayable(&info)?; @@ -52,6 +51,7 @@ pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result = vec![]; + // what incentives are going to mutate when claiming rewards. Not used/returned when querying rewards. let mut modified_incentives: HashMap = HashMap::new(); for incentive in incentives { + // skip expired incentives if incentive.is_expired(current_epoch_id) { continue; } @@ -160,10 +162,12 @@ pub(crate) fn calculate_rewards( ContractError::IncentiveExhausted ); - rewards.push(Coin { - denom: incentive.incentive_asset.denom.clone(), - amount: reward, - }); + if reward > Uint128::zero() { + rewards.push(Coin { + denom: incentive.incentive_asset.denom.clone(), + amount: reward, + }); + } if is_claim { modified_incentives.insert(incentive.identifier.clone(), reward); diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 4ba69f448..47cc3ab67 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -4,7 +4,6 @@ use cosmwasm_std::{ }; use white_whale_std::coin::{get_subdenom, is_factory_token}; -use white_whale_std::epoch_manager::common::validate_epoch; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; use white_whale_std::incentive_manager::MIN_INCENTIVE_AMOUNT; use white_whale_std::incentive_manager::{Curve, Incentive, IncentiveParams}; @@ -21,7 +20,6 @@ use crate::ContractError; pub(crate) fn fill_incentive( deps: DepsMut, - env: Env, info: MessageInfo, params: IncentiveParams, ) -> Result { @@ -32,19 +30,18 @@ pub(crate) fn fill_incentive( if let Ok(incentive) = incentive_result { // the incentive exists, try to expand it - return expand_incentive(deps, env, info, incentive, params); + return expand_incentive(deps, info, incentive, params); } // the incentive does not exist, try to create it } // if no identifier was passed in the params or if the incentive does not exist, try to create the incentive - create_incentive(deps, env, info, params) + create_incentive(deps, info, params) } /// Creates an incentive with the given params fn create_incentive( deps: DepsMut, - env: Env, info: MessageInfo, mut params: IncentiveParams, ) -> Result { @@ -61,7 +58,6 @@ fn create_incentive( deps.as_ref(), config.epoch_manager_addr.clone().into_string(), )?; - validate_epoch(¤t_epoch, env.block.time)?; let (expired_incentives, incentives): (Vec<_>, Vec<_>) = incentives .into_iter() @@ -173,20 +169,14 @@ pub(crate) fn close_incentive( ) -> Result { cw_utils::nonpayable(&info)?; - // validate that user is allowed to close the incentive. Only the incentive creator or the owner of the contract can close an incentive - let config = CONFIG.load(deps.storage)?; - let current_epoch = white_whale_std::epoch_manager::common::get_current_epoch( - deps.as_ref(), - config.epoch_manager_addr.into_string(), - )?; - + // validate that user is allowed to close the incentive. Only the incentive creator or the owner + // of the contract can close an incentive let incentive = get_incentive_by_identifier(deps.storage, &incentive_identifier)?; - if !(!incentive.is_expired(current_epoch.id) - && (incentive.owner == info.sender || cw_ownable::is_owner(deps.storage, &info.sender)?)) - { - return Err(ContractError::Unauthorized); - } + ensure!( + incentive.owner == info.sender || cw_ownable::is_owner(deps.storage, &info.sender)?, + ContractError::Unauthorized + ); Ok(Response::default() .add_messages(close_incentives(deps.storage, vec![incentive])?) @@ -228,7 +218,6 @@ fn close_incentives( /// Expands an incentive with the given params fn expand_incentive( deps: DepsMut, - _env: Env, info: MessageInfo, mut incentive: Incentive, params: IncentiveParams, @@ -267,6 +256,7 @@ fn expand_incentive( .incentive_asset .amount .checked_add(params.incentive_asset.amount)?; + // todo maybe increase the preliminary_end_epoch? INCENTIVES.save(deps.storage, &incentive.identifier, &incentive)?; Ok(Response::default().add_attributes(vec![ @@ -298,7 +288,7 @@ pub(crate) fn on_epoch_changed( .into_iter() .filter(|asset| { if is_factory_token(asset.denom.as_str()) { - //todo remove this hardcoded uLP and point to the pool manager const + //todo remove this hardcoded uLP and point to the pool manager const, to be moved to the white-whale-std package get_subdenom(asset.denom.as_str()) == "uLP" } else { false diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 3d8038e8d..ae66b3962 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -2,10 +2,11 @@ use cosmwasm_std::{ ensure, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, }; -use white_whale_std::incentive_manager::Position; +use white_whale_std::incentive_manager::{Position, RewardsResponse}; -use crate::helpers::validate_unlocking_duration; +use crate::position::helpers::validate_unlocking_duration; use crate::position::helpers::{calculate_weight, get_latest_address_weight, get_latest_lp_weight}; +use crate::queries::query_rewards; use crate::state::{ get_position, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS_HISTORY, POSITIONS, POSITION_ID_COUNTER, @@ -98,15 +99,14 @@ pub(crate) fn close_position( ) -> Result { cw_utils::nonpayable(&info)?; - //todo do this validation to see if there are pending rewards - //query and check if the user has pending rewards - // let rewards_query_result = get_rewards(deps.as_ref(), info.sender.clone().into_string()); - // if let Ok(rewards_response) = rewards_query_result { - // // can't close a position if there are pending rewards - // if !rewards_response.rewards.is_empty() { - // return Err(ContractError::PendingRewards); - // } - // } + // check if the user has pending rewards. Can't close a position without claiming pending rewards first + let rewards_response = query_rewards(deps.as_ref(), info.sender.clone().into_string())?; + match rewards_response { + RewardsResponse::RewardsResponse { rewards } => { + ensure!(rewards.is_empty(), ContractError::PendingRewards) + } + _ => return Err(ContractError::Unauthorized), + } let mut position = get_position(deps.storage, Some(identifier.clone()))?.ok_or( ContractError::NoPositionFound { @@ -129,9 +129,10 @@ pub(crate) fn close_position( if let Some(lp_asset) = lp_asset { // close position partially - // check if the lp_asset requested to close matches the lp_asset of the position + // make sure the lp_asset requested to close matches the lp_asset of the position, and since + // this is a partial close, the amount requested to close should be less than the amount in the position ensure!( - lp_asset.denom == position.lp_asset.denom, + lp_asset.denom == position.lp_asset.denom && lp_asset.amount < position.lp_asset.amount, ContractError::AssetMismatch ); @@ -208,6 +209,12 @@ pub(crate) fn withdraw_position( let penalty_fee = position.lp_asset.amount * emergency_unlock_penalty; + // sanity check + ensure!( + penalty_fee < position.lp_asset.amount, + ContractError::InvalidEmergencyUnlockPenalty + ); + let penalty = Coin { denom: position.lp_asset.denom.to_string(), amount: penalty_fee, @@ -216,7 +223,6 @@ pub(crate) fn withdraw_position( let whale_lair_addr = CONFIG.load(deps.storage)?.whale_lair_addr; // send penalty to whale lair for distribution - //todo the whale lair needs to withdraw the LP tokens from the corresponding pool when this happens messages.push(white_whale_std::whale_lair::fill_rewards_msg_coin( whale_lair_addr.into_string(), vec![penalty], diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index bb7f7e21a..da5293a8b 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Addr, Coin, Decimal256, Order, StdError, Storage, Uint128}; -use white_whale_std::incentive_manager::EpochId; +use white_whale_std::incentive_manager::{Config, EpochId}; use crate::state::{ADDRESS_LP_WEIGHT_HISTORY, LP_WEIGHTS_HISTORY}; use crate::ContractError; @@ -99,3 +99,22 @@ fn return_latest_weight( Err(std_err) => Err(std_err.into()), } } + +/// Validates the `unlocking_duration` specified in the position params is within the range specified +/// in the config. +pub(crate) fn validate_unlocking_duration( + config: &Config, + unlocking_duration: u64, +) -> Result<(), ContractError> { + if unlocking_duration < config.min_unlocking_duration + || unlocking_duration > config.max_unlocking_duration + { + return Err(ContractError::InvalidUnlockingDuration { + min: config.min_unlocking_duration, + max: config.max_unlocking_duration, + specified: unlocking_duration, + }); + } + + Ok(()) +} diff --git a/packages/white-whale-std/src/epoch_manager/common.rs b/packages/white-whale-std/src/epoch_manager/common.rs index d1ccbe106..7e065d108 100644 --- a/packages/white-whale-std/src/epoch_manager/common.rs +++ b/packages/white-whale-std/src/epoch_manager/common.rs @@ -1,10 +1,10 @@ use cosmwasm_std::{Deps, StdError, StdResult, Timestamp}; use crate::constants::DAY_SECONDS; -use crate::epoch_manager::epoch_manager::{EpochResponse, EpochV2, QueryMsg}; +use crate::epoch_manager::epoch_manager::{Epoch, EpochResponse, QueryMsg}; /// Queries the current epoch from the epoch manager contract -pub fn get_current_epoch(deps: Deps, epoch_manager_addr: String) -> StdResult { +pub fn get_current_epoch(deps: Deps, epoch_manager_addr: String) -> StdResult { let epoch_response: EpochResponse = deps .querier .query_wasm_smart(epoch_manager_addr, &QueryMsg::CurrentEpoch {})?; @@ -13,7 +13,7 @@ pub fn get_current_epoch(deps: Deps, epoch_manager_addr: String) -> StdResult StdResult<()> { +pub fn validate_epoch(epoch: &Epoch, current_time: Timestamp) -> StdResult<()> { if current_time .minus_seconds(epoch.start_time.seconds()) .seconds() diff --git a/packages/white-whale-std/src/epoch_manager/epoch_manager.rs b/packages/white-whale-std/src/epoch_manager/epoch_manager.rs index acaf6a8ab..744d61000 100644 --- a/packages/white-whale-std/src/epoch_manager/epoch_manager.rs +++ b/packages/white-whale-std/src/epoch_manager/epoch_manager.rs @@ -8,7 +8,7 @@ use cw_controllers::HooksResponse; #[cw_serde] pub struct InstantiateMsg { - pub start_epoch: EpochV2, + pub start_epoch: Epoch, pub epoch_config: EpochConfig, } @@ -76,12 +76,12 @@ pub struct ConfigResponse { #[cw_serde] pub struct EpochResponse { - pub epoch: EpochV2, + pub epoch: Epoch, } #[cw_serde] pub struct ClaimableEpochsResponse { - pub epochs: Vec, + pub epochs: Vec, } #[cw_serde] @@ -104,20 +104,20 @@ impl Display for EpochConfig { #[cw_serde] #[derive(Default)] -pub struct EpochV2 { +pub struct Epoch { // Epoch identifier pub id: u64, // Epoch start time pub start_time: Timestamp, } -impl EpochV2 { +impl Epoch { pub fn to_epoch_response(self) -> EpochResponse { EpochResponse { epoch: self } } } -impl Display for EpochV2 { +impl Display for Epoch { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, diff --git a/packages/white-whale-std/src/epoch_manager/hooks.rs b/packages/white-whale-std/src/epoch_manager/hooks.rs index 74a6b66ca..911c95ca1 100644 --- a/packages/white-whale-std/src/epoch_manager/hooks.rs +++ b/packages/white-whale-std/src/epoch_manager/hooks.rs @@ -1,11 +1,11 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{to_json_binary, Binary, CosmosMsg, StdResult, WasmMsg}; -use crate::epoch_manager::epoch_manager::EpochV2; +use crate::epoch_manager::epoch_manager::Epoch; #[cw_serde] pub struct EpochChangedHookMsg { - pub current_epoch: EpochV2, + pub current_epoch: Epoch, } impl EpochChangedHookMsg { From 4bc1404bb3d13c4ca0a6697bc96d3f37b83cd74f Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 5 Apr 2024 11:39:13 +0100 Subject: [PATCH 22/35] test: add suite for testing incentive manager --- Cargo.lock | 5 + Cargo.toml | 1 + .../incentive-manager/Cargo.toml | 5 + .../incentive-manager/tests/common/mod.rs | 4 + .../incentive-manager/tests/common/suite.rs | 560 ++++++++++++++++++ .../tests/common/suite_contracts.rs | 51 ++ .../incentive-manager/tests/integration.rs | 163 +++++ 7 files changed, 789 insertions(+) create mode 100644 contracts/liquidity_hub/incentive-manager/tests/common/mod.rs create mode 100644 contracts/liquidity_hub/incentive-manager/tests/common/suite.rs create mode 100644 contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs create mode 100644 contracts/liquidity_hub/incentive-manager/tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index 07f5ee02a..2f7badfcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,6 +746,7 @@ dependencies = [ name = "incentive-manager" version = "0.1.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", @@ -755,11 +756,15 @@ dependencies = [ "cw2", "cw20", "cw20-base", + "epoch-manager", "schemars", "semver", "serde", + "terraswap-pair", "thiserror", + "whale-lair", "white-whale-std", + "white-whale-testing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 923a881f9..6669a8d5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ fee-distributor-mock = { path = "./contracts/liquidity_hub/fee-distributor-mock" incentive-factory = { path = "./contracts/liquidity_hub/pool-network/incentive_factory" } terraswap-token = { path = "./contracts/liquidity_hub/pool-network/terraswap_token" } terraswap-pair = { path = "./contracts/liquidity_hub/pool-network/terraswap_pair" } +epoch-manager = { path = "./contracts/liquidity_hub/epoch-manager" } [workspace.metadata.dylint] libraries = [{ git = "https://github.com/0xFable/cw-lint" }] diff --git a/contracts/liquidity_hub/incentive-manager/Cargo.toml b/contracts/liquidity_hub/incentive-manager/Cargo.toml index 01fc5db92..a9da28871 100644 --- a/contracts/liquidity_hub/incentive-manager/Cargo.toml +++ b/contracts/liquidity_hub/incentive-manager/Cargo.toml @@ -39,3 +39,8 @@ cw-ownable.workspace = true [dev-dependencies] cw-multi-test.workspace = true +white-whale-testing.workspace = true +epoch-manager.workspace = true +whale-lair.workspace = true +terraswap-pair.workspace = true +anyhow.workspace = true diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/mod.rs b/contracts/liquidity_hub/incentive-manager/tests/common/mod.rs new file mode 100644 index 000000000..9987f33e4 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/tests/common/mod.rs @@ -0,0 +1,4 @@ +pub mod suite; +mod suite_contracts; + +pub(crate) const MOCK_CONTRACT_ADDR: &str = "migaloo1wrv0vap0sdpxt3xrdy0yg9ppsx3ppxrfhm6m3s"; diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs new file mode 100644 index 000000000..3c6006703 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -0,0 +1,560 @@ +use cosmwasm_std::testing::MockStorage; +use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Timestamp, Uint128, Uint64}; +use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; +use cw_multi_test::{ + App, AppBuilder, AppResponse, BankKeeper, DistributionKeeper, Executor, FailingModule, + GovFailingModule, IbcFailingModule, StakeKeeper, WasmKeeper, +}; + +use white_whale_std::epoch_manager::epoch_manager::EpochConfig; +use white_whale_std::incentive_manager::{ + Config, IncentiveAction, IncentivesBy, IncentivesResponse, InstantiateMsg, PositionAction, + PositionsResponse, RewardsResponse, +}; +use white_whale_std::pool_network::asset::{Asset, AssetInfo, PairType}; +use white_whale_std::pool_network::pair::ExecuteMsg::ProvideLiquidity; +use white_whale_std::pool_network::pair::{PoolFee, SimulationResponse}; +use white_whale_testing::multi_test::stargate_mock::StargateMock; + +use crate::common::suite_contracts::{ + epoch_manager_contract, incentive_manager_contract, pair_contract, whale_lair_contract, +}; +use crate::common::MOCK_CONTRACT_ADDR; + +type OsmosisTokenFactoryApp = App< + BankKeeper, + MockApiBech32, + MockStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + IbcFailingModule, + GovFailingModule, + StargateMock, +>; + +pub struct TestingSuite { + app: OsmosisTokenFactoryApp, + pub senders: [Addr; 3], + pub incentive_manager_addr: Addr, + pub whale_lair_addr: Addr, + pub epoch_manager_addr: Addr, + pub pools: Vec, +} + +/// TestingSuite helpers +impl TestingSuite { + pub(crate) fn creator(&mut self) -> Addr { + self.senders.first().unwrap().clone() + } + + pub(crate) fn set_time(&mut self, timestamp: Timestamp) -> &mut Self { + let mut block_info = self.app.block_info(); + block_info.time = timestamp; + self.app.set_block(block_info); + + self + } + + #[track_caller] + pub(crate) fn create_pool( + &mut self, + asset_infos: [AssetInfo; 2], + asset_decimals: [u8; 2], + pool_fees: PoolFee, + pair_type: PairType, + token_factory_lp: bool, + ) -> &mut Self { + let pair_id = self.app.store_code(pair_contract()); + let fee_collector = self.create_epoch_manager(); + + // create whale lair + let msg = white_whale_std::pool_network::pair::InstantiateMsg { + asset_infos, + token_code_id: 100, //dummy value, we are using token factory + asset_decimals, + pool_fees, + fee_collector_addr: MOCK_CONTRACT_ADDR.to_string(), // doesn't matter, not gonna interact with it + pair_type, + token_factory_lp, + }; + + let creator = self.creator().clone(); + + self.pools.append(&mut vec![self + .app + .instantiate_contract( + pair_id, + creator.clone(), + &msg, + &[], + "pool", + Some(creator.into_string()), + ) + .unwrap()]); + + self + } +} + +/// Instantiate +impl TestingSuite { + pub(crate) fn default_with_balances(initial_balance: Vec) -> Self { + let sender_1 = Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"); + let sender_2 = Addr::unchecked("migaloo193lk767456jhkzddnz7kf5jvuzfn67gyfvhc40"); + let sender_3 = Addr::unchecked("migaloo1ludaslnu24p5eftw499f7ngsc2jkzqdsrvxt75"); + + let bank = BankKeeper::new(); + + let balances = vec![ + (sender_1.clone(), initial_balance.clone()), + (sender_2.clone(), initial_balance.clone()), + (sender_3.clone(), initial_balance.clone()), + ]; + + let app = AppBuilder::new() + .with_api(MockApiBech32::new("migaloo")) + .with_wasm(WasmKeeper::default().with_address_generator(MockAddressGenerator)) + .with_bank(bank) + .with_stargate(StargateMock {}) + .build(|router, _api, storage| { + balances.into_iter().for_each(|(account, amount)| { + router.bank.init_balance(storage, &account, amount).unwrap() + }); + }); + + Self { + app, + senders: [sender_1, sender_2, sender_3], + incentive_manager_addr: Addr::unchecked(""), + whale_lair_addr: Addr::unchecked(""), + epoch_manager_addr: Addr::unchecked(""), + pools: vec![], + } + } + + #[track_caller] + pub(crate) fn instantiate_default(&mut self) -> &mut Self { + self.create_whale_lair(); + self.create_epoch_manager(); + + // April 4th 2024 15:00:00 UTC + let timestamp = Timestamp::from_seconds(1712242800u64); + self.set_time(timestamp); + + // instantiates the incentive manager contract + self.instantiate( + self.whale_lair_addr.to_string(), + self.epoch_manager_addr.to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_000u128), + }, + 7, + 14, + 86_400, + 31_536_000, + Decimal::percent(10), //10% penalty + ) + } + + fn create_whale_lair(&mut self) { + let whale_lair_id = self.app.store_code(whale_lair_contract()); + + // create whale lair + let msg = white_whale_std::whale_lair::InstantiateMsg { + unbonding_period: Uint64::new(86400u64), + growth_rate: Decimal::one(), + bonding_assets: vec![ + AssetInfo::NativeToken { + denom: "bWHALE".to_string(), + }, + AssetInfo::NativeToken { + denom: "ampWHALE".to_string(), + }, + ], + }; + + let creator = self.creator().clone(); + + self.whale_lair_addr = self + .app + .instantiate_contract( + whale_lair_id, + creator.clone(), + &msg, + &[], + "White Whale Lair".to_string(), + Some(creator.to_string()), + ) + .unwrap(); + } + + fn create_epoch_manager(&mut self) { + let epoch_manager_contract = self.app.store_code(epoch_manager_contract()); + + // create epoch manager + let msg = white_whale_std::epoch_manager::epoch_manager::InstantiateMsg { + start_epoch: Default::default(), + epoch_config: EpochConfig { + duration: Uint64::new(86400_000000000u64), + genesis_epoch: Uint64::new(1712242800_000000000u64), // April 4th 2024 15:00:00 UTC + }, + }; + + let creator = self.creator().clone(); + + self.epoch_manager_addr = self + .app + .instantiate_contract( + epoch_manager_contract, + creator.clone(), + &msg, + &[], + "Epoch Manager".to_string(), + Some(creator.to_string()), + ) + .unwrap(); + } + + #[track_caller] + pub(crate) fn instantiate( + &mut self, + whale_lair_addr: String, + epoch_manager_addr: String, + create_incentive_fee: Coin, + max_concurrent_incentives: u32, + max_incentive_epoch_buffer: u32, + min_unlocking_duration: u64, + max_unlocking_duration: u64, + emergency_unlock_penalty: Decimal, + ) -> &mut Self { + let msg = InstantiateMsg { + owner: self.creator().to_string(), + epoch_manager_addr, + whale_lair_addr, + create_incentive_fee, + max_concurrent_incentives, + max_incentive_epoch_buffer, + min_unlocking_duration, + max_unlocking_duration, + emergency_unlock_penalty, + }; + + let incentive_manager_id = self.app.store_code(incentive_manager_contract()); + + let creator = self.creator().clone(); + + self.incentive_manager_addr = self + .app + .instantiate_contract( + incentive_manager_id, + creator.clone(), + &msg, + &[], + "WW Incentive Manager", + Some(creator.into_string()), + ) + .unwrap(); + self + } + + #[track_caller] + pub(crate) fn instantiate_err( + &mut self, + whale_lair_addr: String, + epoch_manager_addr: String, + create_incentive_fee: Coin, + max_concurrent_incentives: u32, + max_incentive_epoch_buffer: u32, + min_unlocking_duration: u64, + max_unlocking_duration: u64, + emergency_unlock_penalty: Decimal, + result: impl Fn(anyhow::Result), + ) -> &mut Self { + let msg = InstantiateMsg { + owner: self.creator().to_string(), + epoch_manager_addr, + whale_lair_addr, + create_incentive_fee, + max_concurrent_incentives, + max_incentive_epoch_buffer, + min_unlocking_duration, + max_unlocking_duration, + emergency_unlock_penalty, + }; + + let incentive_manager_id = self.app.store_code(incentive_manager_contract()); + + let creator = self.creator().clone(); + + result(self.app.instantiate_contract( + incentive_manager_id, + creator.clone(), + &msg, + &[], + "WW Incentive Manager", + Some(creator.into_string()), + )); + + self + } +} + +/// execute messages +impl TestingSuite { + #[track_caller] + pub(crate) fn update_ownership( + &mut self, + sender: Addr, + action: cw_ownable::Action, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::incentive_manager::ExecuteMsg::UpdateOwnership(action); + + result( + self.app + .execute_contract(sender, self.incentive_manager_addr.clone(), &msg, &[]), + ); + + self + } + + #[track_caller] + pub(crate) fn update_config( + &mut self, + sender: Addr, + whale_lair_addr: Option, + epoch_manager_addr: Option, + create_incentive_fee: Option, + max_concurrent_incentives: Option, + max_incentive_epoch_buffer: Option, + min_unlocking_duration: Option, + max_unlocking_duration: Option, + emergency_unlock_penalty: Option, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::incentive_manager::ExecuteMsg::UpdateConfig { + whale_lair_addr, + epoch_manager_addr, + create_incentive_fee, + max_concurrent_incentives, + max_incentive_epoch_buffer, + min_unlocking_duration, + max_unlocking_duration, + emergency_unlock_penalty, + }; + + result(self.app.execute_contract( + sender, + self.incentive_manager_addr.clone(), + &msg, + &funds, + )); + + self + } + + #[track_caller] + pub(crate) fn manage_incentive( + &mut self, + sender: Addr, + action: IncentiveAction, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::incentive_manager::ExecuteMsg::ManageIncentive { action }; + + result(self.app.execute_contract( + sender, + self.incentive_manager_addr.clone(), + &msg, + &funds, + )); + + self + } + + #[track_caller] + pub(crate) fn manage_position( + &mut self, + sender: Addr, + action: PositionAction, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::incentive_manager::ExecuteMsg::ManagePosition { action }; + + result(self.app.execute_contract( + sender, + self.incentive_manager_addr.clone(), + &msg, + &funds, + )); + + self + } + + #[track_caller] + pub(crate) fn claim( + &mut self, + sender: Addr, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::incentive_manager::ExecuteMsg::Claim; + + result(self.app.execute_contract( + sender, + self.incentive_manager_addr.clone(), + &msg, + &funds, + )); + + self + } +} + +/// queries +impl TestingSuite { + pub(crate) fn query_ownership( + &mut self, + result: impl Fn(StdResult>), + ) -> &mut Self { + let ownership_response: StdResult> = + self.app.wrap().query_wasm_smart( + &self.incentive_manager_addr, + &white_whale_std::incentive_manager::QueryMsg::Ownership {}, + ); + + result(ownership_response); + + self + } + + #[track_caller] + pub(crate) fn query_config(&mut self, result: impl Fn(StdResult)) -> &mut Self { + let response: StdResult = self.app.wrap().query_wasm_smart( + &self.incentive_manager_addr, + &white_whale_std::incentive_manager::QueryMsg::Config {}, + ); + + result(response); + + self + } + + #[track_caller] + pub(crate) fn query_incentives( + &mut self, + filter_by: Option, + start_after: Option, + limit: Option, + result: impl Fn(StdResult), + ) -> &mut Self { + let incentives_response: StdResult = self.app.wrap().query_wasm_smart( + &self.incentive_manager_addr, + &white_whale_std::incentive_manager::QueryMsg::Incentives { + filter_by, + start_after, + limit, + }, + ); + + result(incentives_response); + + self + } + + #[track_caller] + pub(crate) fn query_positions( + &mut self, + address: Addr, + open_state: Option, + result: impl Fn(StdResult), + ) -> &mut Self { + let positions_response: StdResult = self.app.wrap().query_wasm_smart( + &self.incentive_manager_addr, + &white_whale_std::incentive_manager::QueryMsg::Positions { + address: address.to_string(), + open_state, + }, + ); + + result(positions_response); + + self + } + #[track_caller] + pub(crate) fn query_rewards( + &mut self, + address: Addr, + result: impl Fn(StdResult), + ) -> &mut Self { + let rewards_response: StdResult = self.app.wrap().query_wasm_smart( + &self.incentive_manager_addr, + &white_whale_std::incentive_manager::QueryMsg::Rewards { + address: address.to_string(), + }, + ); + + result(rewards_response); + + self + } + + #[track_caller] + pub(crate) fn query_balance( + &mut self, + denom: String, + address: Addr, + result: impl Fn(Uint128), + ) -> &mut Self { + let balance_response = self.app.wrap().query_balance(address, denom.clone()); + result(balance_response.unwrap_or(coin(0, denom)).amount); + + self + } +} + +// pools interactions +impl TestingSuite { + #[track_caller] + pub(crate) fn provide_liquidity( + &mut self, + sender: Addr, + assets: [Asset; 2], + pool: Addr, + funds: &[Coin], + result: impl Fn(Result), + ) -> &mut Self { + let msg = ProvideLiquidity { + assets, + slippage_tolerance: None, + receiver: None, + }; + + result(self.app.execute_contract(sender, pool, &msg, funds)); + + self + } + + #[track_caller] + pub(crate) fn simulate_swap( + &mut self, + offer_asset: Asset, + pool: Addr, + result: impl Fn(StdResult), + ) -> &mut Self { + let response: StdResult = self.app.wrap().query_wasm_smart( + pool, + &white_whale_std::pool_network::pair::QueryMsg::Simulation { offer_asset }, + ); + + result(response); + + self + } +} diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs new file mode 100644 index 000000000..859d4e5c4 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::Empty; +use cw_multi_test::{Contract, ContractWrapper}; + +/// Creates the incentive manager contract +pub fn incentive_manager_contract() -> Box> { + let contract = ContractWrapper::new( + incentive_manager::contract::execute, + incentive_manager::contract::instantiate, + incentive_manager::contract::query, + ) + .with_migrate(incentive_manager::contract::migrate); + + Box::new(contract) +} + +/// Creates the whale lair contract +pub fn whale_lair_contract() -> Box> { + let contract = ContractWrapper::new( + whale_lair::contract::execute, + whale_lair::contract::instantiate, + whale_lair::contract::query, + ) + .with_migrate(whale_lair::contract::migrate); + + Box::new(contract) +} + +/// Creates the epoch manager contract +pub fn epoch_manager_contract() -> Box> { + let contract = ContractWrapper::new( + epoch_manager::contract::execute, + epoch_manager::contract::instantiate, + epoch_manager::contract::query, + ) + .with_migrate(epoch_manager::contract::migrate); + + Box::new(contract) +} + +/// Creates a pair contract +pub fn pair_contract() -> Box> { + let contract = ContractWrapper::new( + terraswap_pair::contract::execute, + terraswap_pair::contract::instantiate, + terraswap_pair::contract::query, + ) + .with_reply(terraswap_pair::contract::reply) + .with_migrate(terraswap_pair::contract::migrate); + + Box::new(contract) +} diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs new file mode 100644 index 000000000..119bfdbc1 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -0,0 +1,163 @@ +extern crate core; + +use std::cell::RefCell; + +use cosmwasm_std::{ + coin, coins, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Uint128, WasmMsg, +}; +use cw_ownable::OwnershipError; +use incentive_manager::ContractError; + +use white_whale_std::fee::Fee; +use white_whale_std::pool_network::asset::{Asset, AssetInfo, PairType}; +use white_whale_std::pool_network::pair::PoolFee; +use white_whale_std::vault_manager::{ + AssetQueryParams, FilterVaultBy, PaybackAssetResponse, VaultFee, +}; + +use crate::common::suite::TestingSuite; +use crate::common::MOCK_CONTRACT_ADDR; + +mod common; + +#[test] +fn instantiate_incentive_manager() { + let mut suite = + TestingSuite::default_with_balances(vec![coin(1_000_000_000u128, "uwhale".to_string())]); + + suite.instantiate_err( + MOCK_CONTRACT_ADDR.to_string(), + MOCK_CONTRACT_ADDR.to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_000u128), + }, + 0, + 14, + 86_400, + 31_536_000, + Decimal::percent(10), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::UnspecifiedConcurrentIncentives { .. } => {} + _ => panic!("Wrong error type, should return ContractError::UnspecifiedConcurrentIncentives"), + } + } + ).instantiate_err( + MOCK_CONTRACT_ADDR.to_string(), + MOCK_CONTRACT_ADDR.to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_000u128), + }, + 1, + 14, + 86_400, + 86_399, + Decimal::percent(10), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::InvalidUnbondingRange { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), + } + } + ).instantiate_err( + MOCK_CONTRACT_ADDR.to_string(), + MOCK_CONTRACT_ADDR.to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_000u128), + }, + 1, + 14, + 86_400, + 86_500, + Decimal::percent(101), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::InvalidEmergencyUnlockPenalty { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidEmergencyUnlockPenalty"), + } + } + ).instantiate( + MOCK_CONTRACT_ADDR.to_string(), + MOCK_CONTRACT_ADDR.to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_000u128), + }, + 7, + 14, + 86_400, + 31_536_000, + Decimal::percent(10), //10% penalty + ); +} + +#[test] +fn verify_ownership() { + let mut suite = TestingSuite::default_with_balances(vec![]); + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let unauthorized = suite.senders[2].clone(); + + suite + .instantiate_default() + .query_ownership(|result| { + let ownership = result.unwrap(); + assert_eq!(Addr::unchecked(ownership.owner.unwrap()), creator); + }) + .update_ownership( + unauthorized, + cw_ownable::Action::TransferOwnership { + new_owner: other.to_string(), + expiry: None, + }, + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::OwnershipError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::OwnershipError"), + } + }, + ) + .update_ownership( + creator, + cw_ownable::Action::TransferOwnership { + new_owner: other.to_string(), + expiry: None, + }, + |result| { + result.unwrap(); + }, + ) + .update_ownership( + other.clone(), + cw_ownable::Action::AcceptOwnership, + |result| { + result.unwrap(); + }, + ) + .query_ownership(|result| { + let ownership = result.unwrap(); + assert_eq!(Addr::unchecked(ownership.owner.unwrap()), other); + }) + .update_ownership( + other.clone(), + cw_ownable::Action::RenounceOwnership, + |result| { + result.unwrap(); + }, + ) + .query_ownership(|result| { + let ownership = result.unwrap(); + assert!(ownership.owner.is_none()); + }); +} From 4c10c71adbc420ef0536f17240c9b87553438a89 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 5 Apr 2024 12:54:45 +0100 Subject: [PATCH 23/35] test: add tests for creating incentives --- Cargo.lock | 1 - .../incentive-manager/Cargo.toml | 1 - .../incentive-manager/src/contract.rs | 4 +- .../incentive-manager/src/helpers.rs | 2 +- .../incentive-manager/src/manager/commands.rs | 4 +- .../incentive-manager/src/state.rs | 3 +- .../incentive-manager/tests/common/suite.rs | 121 +++-- .../tests/common/suite_contracts.rs | 13 - .../incentive-manager/tests/integration.rs | 440 +++++++++++++++++- 9 files changed, 492 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f7badfcd..3fa8d9054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -760,7 +760,6 @@ dependencies = [ "schemars", "semver", "serde", - "terraswap-pair", "thiserror", "whale-lair", "white-whale-std", diff --git a/contracts/liquidity_hub/incentive-manager/Cargo.toml b/contracts/liquidity_hub/incentive-manager/Cargo.toml index a9da28871..e9ff898d3 100644 --- a/contracts/liquidity_hub/incentive-manager/Cargo.toml +++ b/contracts/liquidity_hub/incentive-manager/Cargo.toml @@ -42,5 +42,4 @@ cw-multi-test.workspace = true white-whale-testing.workspace = true epoch-manager.workspace = true whale-lair.workspace = true -terraswap-pair.workspace = true anyhow.workspace = true diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index 0c108fded..e70edcaa9 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -11,7 +11,7 @@ use white_whale_std::vault_manager::MigrateMsg; use crate::error::ContractError; use crate::helpers::validate_emergency_unlock_penalty; -use crate::state::CONFIG; +use crate::state::{CONFIG, INCENTIVE_COUNTER}; use crate::{incentive, manager, position, queries}; const CONTRACT_NAME: &str = "white-whale_incentive-manager"; @@ -53,7 +53,7 @@ pub fn instantiate( }; CONFIG.save(deps.storage, &config)?; - + INCENTIVE_COUNTER.save(deps.storage, &0)?; cw_ownable::initialize_owner(deps.storage, deps.api, Some(msg.owner.as_str()))?; Ok(Response::default().add_attributes(vec![ diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index efd36f871..8f22275c0 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -133,7 +133,7 @@ pub(crate) fn validate_incentive_epochs( // ensure that start date is before end date ensure!( - start_epoch <= preliminary_end_epoch, + start_epoch < preliminary_end_epoch, ContractError::IncentiveStartTimeAfterEndTime ); diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 47cc3ab67..2ca943b62 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -115,7 +115,9 @@ fn create_incentive( .incentive_identifier .unwrap_or(incentive_id.to_string()); - // make sure another incentive with the same identifier doesn't exist + // sanity check. Make sure another incentive with the same identifier doesn't exist. Theoretically this should + // never happen, since the fill_incentive function would try to expand the incentive if a user tries + // filling an incentive with an identifier that already exists ensure!( get_incentive_by_identifier(deps.storage, &incentive_identifier).is_err(), ContractError::IncentiveAlreadyExists diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index d09ce9659..698cefb8d 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -1,3 +1,4 @@ +use std::clone::Clone; use std::string::ToString; use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; @@ -60,7 +61,7 @@ pub const INCENTIVES: IndexedMap<&str, Incentive, IncentiveIndexes> = IndexedMap "incentives__lp_asset", ), incentive_asset: MultiIndex::new( - |_pk, i| i.incentive_asset.to_string(), + |_pk, i| i.incentive_asset.denom.clone(), "incentives", "incentives__incentive_asset", ), diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index 3c6006703..526c7ca35 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -6,7 +6,7 @@ use cw_multi_test::{ GovFailingModule, IbcFailingModule, StakeKeeper, WasmKeeper, }; -use white_whale_std::epoch_manager::epoch_manager::EpochConfig; +use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig, EpochResponse}; use white_whale_std::incentive_manager::{ Config, IncentiveAction, IncentivesBy, IncentivesResponse, InstantiateMsg, PositionAction, PositionsResponse, RewardsResponse, @@ -17,7 +17,7 @@ use white_whale_std::pool_network::pair::{PoolFee, SimulationResponse}; use white_whale_testing::multi_test::stargate_mock::StargateMock; use crate::common::suite_contracts::{ - epoch_manager_contract, incentive_manager_contract, pair_contract, whale_lair_contract, + epoch_manager_contract, incentive_manager_contract, whale_lair_contract, }; use crate::common::MOCK_CONTRACT_ADDR; @@ -56,46 +56,6 @@ impl TestingSuite { self } - - #[track_caller] - pub(crate) fn create_pool( - &mut self, - asset_infos: [AssetInfo; 2], - asset_decimals: [u8; 2], - pool_fees: PoolFee, - pair_type: PairType, - token_factory_lp: bool, - ) -> &mut Self { - let pair_id = self.app.store_code(pair_contract()); - let fee_collector = self.create_epoch_manager(); - - // create whale lair - let msg = white_whale_std::pool_network::pair::InstantiateMsg { - asset_infos, - token_code_id: 100, //dummy value, we are using token factory - asset_decimals, - pool_fees, - fee_collector_addr: MOCK_CONTRACT_ADDR.to_string(), // doesn't matter, not gonna interact with it - pair_type, - token_factory_lp, - }; - - let creator = self.creator().clone(); - - self.pools.append(&mut vec![self - .app - .instantiate_contract( - pair_id, - creator.clone(), - &msg, - &[], - "pool", - Some(creator.into_string()), - ) - .unwrap()]); - - self - } } /// Instantiate @@ -151,7 +111,7 @@ impl TestingSuite { denom: "uwhale".to_string(), amount: Uint128::new(1_000u128), }, - 7, + 2, 14, 86_400, 31_536_000, @@ -196,7 +156,10 @@ impl TestingSuite { // create epoch manager let msg = white_whale_std::epoch_manager::epoch_manager::InstantiateMsg { - start_epoch: Default::default(), + start_epoch: Epoch { + id: 10, + start_time: Timestamp::from_nanos(1712242800_000000000u64), + }, epoch_config: EpochConfig { duration: Uint64::new(86400_000000000u64), genesis_epoch: Uint64::new(1712242800_000000000u64), // April 4th 2024 15:00:00 UTC @@ -519,41 +482,75 @@ impl TestingSuite { } } -// pools interactions +/// Epoch manager actions impl TestingSuite { #[track_caller] - pub(crate) fn provide_liquidity( + pub(crate) fn create_epoch( + &mut self, + sender: Addr, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch {}; + + result( + self.app + .execute_contract(sender, self.epoch_manager_addr.clone(), &msg, &vec![]), + ); + + self + } + + #[track_caller] + pub(crate) fn add_hook( &mut self, sender: Addr, - assets: [Asset; 2], - pool: Addr, - funds: &[Coin], + contract_addr: Addr, + funds: Vec, result: impl Fn(Result), ) -> &mut Self { - let msg = ProvideLiquidity { - assets, - slippage_tolerance: None, - receiver: None, + let msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::AddHook { + contract_addr: contract_addr.to_string(), }; - result(self.app.execute_contract(sender, pool, &msg, funds)); + result( + self.app + .execute_contract(sender, self.epoch_manager_addr.clone(), &msg, &funds), + ); self } #[track_caller] - pub(crate) fn simulate_swap( + pub(crate) fn remove_hook( &mut self, - offer_asset: Asset, - pool: Addr, - result: impl Fn(StdResult), + sender: Addr, + contract_addr: Addr, + funds: Vec, + result: impl Fn(Result), ) -> &mut Self { - let response: StdResult = self.app.wrap().query_wasm_smart( - pool, - &white_whale_std::pool_network::pair::QueryMsg::Simulation { offer_asset }, + let msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::RemoveHook { + contract_addr: contract_addr.to_string(), + }; + + result( + self.app + .execute_contract(sender, self.epoch_manager_addr.clone(), &msg, &funds), ); - result(response); + self + } + + #[track_caller] + pub(crate) fn query_current_epoch( + &mut self, + result: impl Fn(StdResult), + ) -> &mut Self { + let current_epoch_response: StdResult = self.app.wrap().query_wasm_smart( + &self.epoch_manager_addr, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + ); + + result(current_epoch_response); self } diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs index 859d4e5c4..cd4191494 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs @@ -36,16 +36,3 @@ pub fn epoch_manager_contract() -> Box> { Box::new(contract) } - -/// Creates a pair contract -pub fn pair_contract() -> Box> { - let contract = ContractWrapper::new( - terraswap_pair::contract::execute, - terraswap_pair::contract::instantiate, - terraswap_pair::contract::query, - ) - .with_reply(terraswap_pair::contract::reply) - .with_migrate(terraswap_pair::contract::migrate); - - Box::new(contract) -} diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index 119bfdbc1..704dba203 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -1,19 +1,9 @@ extern crate core; -use std::cell::RefCell; +use cosmwasm_std::{coin, Addr, Coin, Decimal, Uint128}; -use cosmwasm_std::{ - coin, coins, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Uint128, WasmMsg, -}; -use cw_ownable::OwnershipError; use incentive_manager::ContractError; - -use white_whale_std::fee::Fee; -use white_whale_std::pool_network::asset::{Asset, AssetInfo, PairType}; -use white_whale_std::pool_network::pair::PoolFee; -use white_whale_std::vault_manager::{ - AssetQueryParams, FilterVaultBy, PaybackAssetResponse, VaultFee, -}; +use white_whale_std::incentive_manager::{IncentiveAction, IncentiveParams, IncentivesBy}; use crate::common::suite::TestingSuite; use crate::common::MOCK_CONTRACT_ADDR; @@ -44,7 +34,7 @@ fn instantiate_incentive_manager() { ContractError::UnspecifiedConcurrentIncentives { .. } => {} _ => panic!("Wrong error type, should return ContractError::UnspecifiedConcurrentIncentives"), } - } + }, ).instantiate_err( MOCK_CONTRACT_ADDR.to_string(), MOCK_CONTRACT_ADDR.to_string(), @@ -64,7 +54,7 @@ fn instantiate_incentive_manager() { ContractError::InvalidUnbondingRange { .. } => {} _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), } - } + }, ).instantiate_err( MOCK_CONTRACT_ADDR.to_string(), MOCK_CONTRACT_ADDR.to_string(), @@ -84,7 +74,7 @@ fn instantiate_incentive_manager() { ContractError::InvalidEmergencyUnlockPenalty { .. } => {} _ => panic!("Wrong error type, should return ContractError::InvalidEmergencyUnlockPenalty"), } - } + }, ).instantiate( MOCK_CONTRACT_ADDR.to_string(), MOCK_CONTRACT_ADDR.to_string(), @@ -161,3 +151,423 @@ fn verify_ownership() { assert!(ownership.owner.is_none()); }); } + +#[test] +fn create_incentives() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + + // suite + // .instantiate_default() + // .query_current_epoch(|result| { + // let current_epoch = result.unwrap(); + // println!("Current epoch: {:?}", current_epoch); + // }); + + // try all misconfigurations when creating an incentive + suite + .instantiate_default() + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(25), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Default::default(), + }, + incentive_identifier: None, + }, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::InvalidIncentiveAmount { .. } => {} + _ => panic!( + "Wrong error type, should return ContractError::InvalidIncentiveAmount" + ), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(25), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(2_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(2_000, "ulab")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::IncentiveFeeMissing { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::IncentiveFeeMissing") + } + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(25), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(5_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(8_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(25), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(2_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(25), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(2_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(5_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(25), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(5_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(25), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::IncentiveStartTimeAfterEndTime { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveStartTimeAfterEndTime"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(8), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::IncentiveEndsInPast { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveEndsInPast"), + } + }, + ).manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(15), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::IncentiveStartTimeAfterEndTime { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveStartTimeAfterEndTime"), + } + }, + ).manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(20), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::IncentiveStartTimeAfterEndTime { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveStartTimeAfterEndTime"), + } + }, + ).manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(30), + preliminary_end_epoch: Some(35), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::IncentiveStartTooFar { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveStartTooFar"), + } + }, + ); + + // create an incentive properly + suite + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(10_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(10_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + // should fail, max incentives per lp_denom was set to 2 in the instantiate_default + // function + match err { + ContractError::TooManyIncentives { .. } => {} + _ => panic!("Wrong error type, should return ContractError::TooManyIncentives"), + } + }, + ) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 2); + }) + .query_incentives( + Some(IncentivesBy::Identifier("incentive_1".to_string())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 1); + assert_eq!( + incentives_response.incentives[0].incentive_asset, + Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000) + } + ); + }, + ) + .query_incentives( + Some(IncentivesBy::Identifier("2".to_string())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 1); + assert_eq!( + incentives_response.incentives[0].incentive_asset, + Coin { + denom: "ulab".to_string(), + amount: Uint128::new(10_000) + } + ); + }, + ) + .query_incentives( + Some(IncentivesBy::IncentiveAsset("ulab".to_string())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 2); + }, + ) + .query_incentives( + Some(IncentivesBy::LPDenom(lp_denom.clone())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 2); + }, + ); +} From 45a7b854bd26d178eacca70d224f2eb41dd1839b Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 5 Apr 2024 13:28:20 +0100 Subject: [PATCH 24/35] test: add tests for expanding incentive --- .../incentive-manager/src/manager/commands.rs | 17 +- .../incentive-manager/tests/integration.rs | 178 +++++++++++++++++- .../vault-manager/tests/units.rs | 47 +++++ .../white-whale-std/src/incentive_manager.rs | 7 +- 4 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 contracts/liquidity_hub/vault-manager/tests/units.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 2ca943b62..709cd269f 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ ensure, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdError, - Storage, Uint128, + Storage, Uint128, Uint64, }; use white_whale_std::coin::{get_subdenom, is_factory_token}; @@ -235,7 +235,7 @@ fn expand_incentive( // check if the incentive has already expired, can't be expanded ensure!( - incentive.is_expired(current_epoch.id), + !incentive.is_expired(current_epoch.id), ContractError::IncentiveAlreadyExpired ); @@ -258,7 +258,18 @@ fn expand_incentive( .incentive_asset .amount .checked_add(params.incentive_asset.amount)?; - // todo maybe increase the preliminary_end_epoch? + + let additional_epochs = params + .incentive_asset + .amount + .checked_div(incentive.emission_rate)?; + + // adjust the preliminary end_epoch + incentive.preliminary_end_epoch = incentive + .preliminary_end_epoch + .checked_add(Uint64::try_from(additional_epochs)?.u64()) + .ok_or(ContractError::InvalidEndEpoch)?; + INCENTIVES.save(deps.storage, &incentive.identifier, &incentive)?; Ok(Response::default().add_attributes(vec![ diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index 704dba203..5103de912 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -166,13 +166,6 @@ fn create_incentives() { let creator = suite.creator(); let other = suite.senders[1].clone(); - // suite - // .instantiate_default() - // .query_current_epoch(|result| { - // let current_epoch = result.unwrap(); - // println!("Current epoch: {:?}", current_epoch); - // }); - // try all misconfigurations when creating an incentive suite .instantiate_default() @@ -571,3 +564,174 @@ fn create_incentives() { }, ); } + +#[test] +fn expand_incentives() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + + suite + .instantiate_default() + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(8_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(4_000, "ulab")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(8_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(8_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_100u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(4_100, "ulab")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::InvalidExpansionAmount { .. } => {} + _ => panic!( + "Wrong error type, should return ContractError::InvalidExpansionAmount" + ), + } + }, + ) + .query_incentives( + Some(IncentivesBy::Identifier("incentive_1".to_string())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + let incentive = incentives_response.incentives[0].clone(); + assert_eq!( + incentive.incentive_asset, + Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000) + } + ); + + assert_eq!(incentive.preliminary_end_epoch, 28); + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(5_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(5_000u128, "ulab")], + |result| { + result.unwrap(); + }, + ) + .query_incentives( + Some(IncentivesBy::Identifier("incentive_1".to_string())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + let incentive = incentives_response.incentives[0].clone(); + assert_eq!( + incentive.incentive_asset, + Coin { + denom: "ulab".to_string(), + amount: Uint128::new(9_000) + } + ); + + assert_eq!(incentive.preliminary_end_epoch, 38); + }, + ); +} diff --git a/contracts/liquidity_hub/vault-manager/tests/units.rs b/contracts/liquidity_hub/vault-manager/tests/units.rs new file mode 100644 index 000000000..67b0e8f01 --- /dev/null +++ b/contracts/liquidity_hub/vault-manager/tests/units.rs @@ -0,0 +1,47 @@ +use cosmwasm_std::{Addr, Coin, Uint128}; +use white_whale_std::incentive_manager::{Curve, Incentive}; + +#[test] +fn incentive_expiration() { + let incentive = Incentive { + identifier: "identifier".to_string(), + owner: Addr::unchecked("owner"), + lp_denom: "lp_denom".to_string(), + incentive_asset: Coin { + denom: "asset".to_string(), + amount: Uint128::new(5_000), + }, + claimed_amount: Uint128::zero(), + emission_rate: Uint128::new(1_000), + curve: Curve::Linear, + start_epoch: 10, + preliminary_end_epoch: 14, + last_epoch_claimed: 9, + }; + + assert!(!incentive.is_expired(9)); + assert!(!incentive.is_expired(12)); + + // expired already after 14 days from the last epoch claimed after the incentive started + assert!(incentive.is_expired(23)); + assert!(incentive.is_expired(33)); + + let incentive = Incentive { + identifier: "identifier".to_string(), + owner: Addr::unchecked("owner"), + lp_denom: "lp_denom".to_string(), + incentive_asset: Coin { + denom: "asset".to_string(), + amount: Uint128::new(5_000), + }, + claimed_amount: Uint128::new(4_001), + emission_rate: Uint128::new(1_000), + curve: Curve::Linear, + start_epoch: 10, + preliminary_end_epoch: 14, + last_epoch_claimed: 9, + }; + + // expired already as incentive_asset - claimed is lower than the MIN_INCENTIVE_AMOUNT + assert!(incentive.is_expired(13)); +} diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index 100033ae2..974961f3e 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -1,7 +1,8 @@ +use std::collections::HashMap; + use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; -use std::collections::HashMap; use crate::epoch_manager::hooks::EpochChangedHookMsg; @@ -234,7 +235,8 @@ impl Incentive { .amount .saturating_sub(self.claimed_amount) < MIN_INCENTIVE_AMOUNT - || epoch_id >= self.last_epoch_claimed + DEFAULT_INCENTIVE_DURATION + || (epoch_id > self.start_epoch + && epoch_id >= self.last_epoch_claimed + DEFAULT_INCENTIVE_DURATION) } } @@ -268,6 +270,7 @@ pub struct Position { /// The owner of the position. pub receiver: Addr, } + #[cw_serde] pub enum RewardsResponse { RewardsResponse { From e6597c430714a575c91e234b153035c7b80c4847 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 5 Apr 2024 13:38:16 +0100 Subject: [PATCH 25/35] test: add test for closing incentives --- .../incentive-manager/tests/integration.rs | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index 5103de912..5e0f47211 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -735,3 +735,143 @@ fn expand_incentives() { }, ); } +#[test] +fn close_incentives() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let another = suite.senders[2].clone(); + + suite + .instantiate_default() + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Close { + incentive_identifier: "incentive_1".to_string(), + }, + vec![coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Close { + incentive_identifier: "incentive_2".to_string(), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::NonExistentIncentive { .. } => {} + _ => panic!( + "Wrong error type, should return ContractError::NonExistentIncentive" + ), + } + }, + ) + .manage_incentive( + another.clone(), + IncentiveAction::Close { + incentive_identifier: "incentive_1".to_string(), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }, + ) + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(99_999_6000)); + }) + .manage_incentive( + other.clone(), + IncentiveAction::Close { + incentive_identifier: "incentive_1".to_string(), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(100_000_0000)); + }); + + suite + .instantiate_default() + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(28), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(99_999_6000)); + }) + // the owner of the contract can also close incentives + .manage_incentive( + creator.clone(), + IncentiveAction::Close { + incentive_identifier: "incentive_1".to_string(), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(100_000_0000)); + }); +} From 6677bfd05e49c1981557cb456439c6283ebc5d0f Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 5 Apr 2024 16:30:35 +0100 Subject: [PATCH 26/35] test: update config tests --- .../incentive-manager/tests/integration.rs | 332 ++++++++++++++---- 1 file changed, 269 insertions(+), 63 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index 5e0f47211..312124d48 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -3,7 +3,7 @@ extern crate core; use cosmwasm_std::{coin, Addr, Coin, Decimal, Uint128}; use incentive_manager::ContractError; -use white_whale_std::incentive_manager::{IncentiveAction, IncentiveParams, IncentivesBy}; +use white_whale_std::incentive_manager::{Config, IncentiveAction, IncentiveParams, IncentivesBy}; use crate::common::suite::TestingSuite; use crate::common::MOCK_CONTRACT_ADDR; @@ -90,68 +90,6 @@ fn instantiate_incentive_manager() { ); } -#[test] -fn verify_ownership() { - let mut suite = TestingSuite::default_with_balances(vec![]); - let creator = suite.creator(); - let other = suite.senders[1].clone(); - let unauthorized = suite.senders[2].clone(); - - suite - .instantiate_default() - .query_ownership(|result| { - let ownership = result.unwrap(); - assert_eq!(Addr::unchecked(ownership.owner.unwrap()), creator); - }) - .update_ownership( - unauthorized, - cw_ownable::Action::TransferOwnership { - new_owner: other.to_string(), - expiry: None, - }, - |result| { - let err = result.unwrap_err().downcast::().unwrap(); - - match err { - ContractError::OwnershipError { .. } => {} - _ => panic!("Wrong error type, should return ContractError::OwnershipError"), - } - }, - ) - .update_ownership( - creator, - cw_ownable::Action::TransferOwnership { - new_owner: other.to_string(), - expiry: None, - }, - |result| { - result.unwrap(); - }, - ) - .update_ownership( - other.clone(), - cw_ownable::Action::AcceptOwnership, - |result| { - result.unwrap(); - }, - ) - .query_ownership(|result| { - let ownership = result.unwrap(); - assert_eq!(Addr::unchecked(ownership.owner.unwrap()), other); - }) - .update_ownership( - other.clone(), - cw_ownable::Action::RenounceOwnership, - |result| { - result.unwrap(); - }, - ) - .query_ownership(|result| { - let ownership = result.unwrap(); - assert!(ownership.owner.is_none()); - }); -} - #[test] fn create_incentives() { let lp_denom = "factory/pool/uLP".to_string(); @@ -875,3 +813,271 @@ fn close_incentives() { assert_eq!(balance, Uint128::new(100_000_0000)); }); } + +#[test] +fn verify_ownership() { + let mut suite = TestingSuite::default_with_balances(vec![]); + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let unauthorized = suite.senders[2].clone(); + + suite + .instantiate_default() + .query_ownership(|result| { + let ownership = result.unwrap(); + assert_eq!(Addr::unchecked(ownership.owner.unwrap()), creator); + }) + .update_ownership( + unauthorized, + cw_ownable::Action::TransferOwnership { + new_owner: other.to_string(), + expiry: None, + }, + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::OwnershipError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::OwnershipError"), + } + }, + ) + .update_ownership( + creator, + cw_ownable::Action::TransferOwnership { + new_owner: other.to_string(), + expiry: None, + }, + |result| { + result.unwrap(); + }, + ) + .update_ownership( + other.clone(), + cw_ownable::Action::AcceptOwnership, + |result| { + result.unwrap(); + }, + ) + .query_ownership(|result| { + let ownership = result.unwrap(); + assert_eq!(Addr::unchecked(ownership.owner.unwrap()), other); + }) + .update_ownership( + other.clone(), + cw_ownable::Action::RenounceOwnership, + |result| { + result.unwrap(); + }, + ) + .query_ownership(|result| { + let ownership = result.unwrap(); + assert!(ownership.owner.is_none()); + }); +} + +#[test] +fn test_epoch_change_hook() {} + +#[test] +pub fn update_config() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let another = suite.senders[2].clone(); + + suite.instantiate_default(); + + let whale_lair = suite.whale_lair_addr.clone(); + let epoch_manager = suite.epoch_manager_addr.clone(); + + let expected_config = Config { + whale_lair_addr: whale_lair, + epoch_manager_addr: epoch_manager, + create_incentive_fee: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_000u128), + }, + max_concurrent_incentives: 2u32, + max_incentive_epoch_buffer: 14u32, + min_unlocking_duration: 86_400u64, + max_unlocking_duration: 31_536_000u64, + emergency_unlock_penalty: Decimal::percent(10), + }; + + suite.query_config(|result| { + let config = result.unwrap(); + assert_eq!(config, expected_config); + }) + .update_config( + other.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(3u32), + Some(15u32), + Some(172_800u64), + Some(864_000u64), + Some(Decimal::percent(50)), + vec![coin(1_000, "uwhale")], + |result|{ + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + } + ) .update_config( + other.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(0u32), + Some(15u32), + Some(172_800u64), + Some(864_000u64), + Some(Decimal::percent(50)), + vec![], + |result|{ + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::OwnershipError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::OwnershipError"), + } + } + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(0u32), + Some(15u32), + Some(172_800u64), + Some(864_000u64), + Some(Decimal::percent(50)), + vec![], + |result|{ + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::UnspecifiedConcurrentIncentives { .. } => {} + _ => panic!("Wrong error type, should return ContractError::UnspecifiedConcurrentIncentives"), + } + } + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(80_800u64), + Some(80_000u64), + Some(Decimal::percent(50)), + vec![], + |result|{ + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidUnbondingRange { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), + } + } + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(300_000u64), + Some(200_000u64), + Some(Decimal::percent(50)), + vec![], + |result|{ + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidUnbondingRange { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), + } + } + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(100_000u64), + Some(200_000u64), + Some(Decimal::percent(105)), + vec![], + |result|{ + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidEmergencyUnlockPenalty { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidEmergencyUnlockPenalty"), + } + } + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(100_000u64), + Some(200_000u64), + Some(Decimal::percent(20)), + vec![], + |result|{ + result.unwrap(); + } + ); + + let expected_config = Config { + whale_lair_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), + epoch_manager_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), + create_incentive_fee: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }, + max_concurrent_incentives: 5u32, + max_incentive_epoch_buffer: 15u32, + min_unlocking_duration: 100_000u64, + max_unlocking_duration: 200_000u64, + emergency_unlock_penalty: Decimal::percent(20), + }; + + suite.query_config(|result| { + let config = result.unwrap(); + assert_eq!(config, expected_config); + }); +} From c41cd3c3643392e375007aede399776620f8cd1c Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 8 Apr 2024 18:09:44 +0100 Subject: [PATCH 27/35] chore: add test to manage position while fixing minor bugs --- .../incentive-manager/src/contract.rs | 3 + .../incentive-manager/src/error.rs | 5 + .../src/incentive/commands.rs | 45 +- .../incentive-manager/src/manager/commands.rs | 40 +- .../src/position/commands.rs | 15 +- .../incentive-manager/src/queries.rs | 23 +- .../incentive-manager/src/state.rs | 22 + .../incentive-manager/tests/common/suite.rs | 31 +- .../incentive-manager/tests/integration.rs | 570 +++++++++++++++++- .../white-whale-std/src/incentive_manager.rs | 17 + 10 files changed, 739 insertions(+), 32 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index e70edcaa9..c16d88754 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -183,6 +183,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { Ok(to_json_binary(&queries::query_rewards(deps, address)?)?) } + QueryMsg::LPWeight { denom, epoch_id } => Ok(to_json_binary(&queries::query_lp_weight( + deps, denom, epoch_id, + )?)?), } } diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 98844f3ff..40e0f607f 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -7,6 +7,8 @@ use cw_utils::PaymentError; use semver::Version; use thiserror::Error; +use white_whale_std::incentive_manager::EpochId; + #[derive(Error, Debug)] pub enum ContractError { #[error("{0}")] @@ -162,6 +164,9 @@ pub enum ContractError { /// The emission rate of the incentive emission_rate: Uint128, }, + + #[error("There's no snapshot of the LP weight in the contract for the epoch {epoch_id}")] + LpWeightNotFound { epoch_id: EpochId }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index 8ecc0ad6a..7b70046e0 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -49,9 +49,16 @@ pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result Result<_, ContractError> { let mut incentive = incentive.unwrap(); + incentive.last_epoch_claimed = current_epoch.id; incentive.claimed_amount = incentive.claimed_amount.checked_add(claimed_reward)?; - incentive.last_epoch_claimed = current_epoch.id; + + // sanity check to make sure an incentive doesn't get drained + ensure!( + incentive.claimed_amount <= incentive.incentive_asset.amount, + ContractError::IncentiveExhausted + ); + Ok(incentive) }, )?; @@ -67,11 +74,18 @@ pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result Result<(HashMap, EpochId), ContractError> { let mut incentive_emissions = HashMap::new(); - let until_epoch = if incentive.preliminary_end_epoch < *current_epoch_id { - incentive.preliminary_end_epoch + let until_epoch = if incentive.preliminary_end_epoch <= *current_epoch_id { + // the preliminary_end_eopch is not inclusive, so we subtract 1 + incentive.preliminary_end_epoch - 1u64 } else { *current_epoch_id }; diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 709cd269f..1cccaa09f 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -13,8 +13,8 @@ use crate::helpers::{ validate_incentive_epochs, }; use crate::state::{ - get_incentive_by_identifier, get_incentives_by_lp_denom, CONFIG, INCENTIVES, INCENTIVE_COUNTER, - LP_WEIGHTS_HISTORY, + get_incentive_by_identifier, get_incentives_by_lp_denom, get_latest_lp_weight_record, CONFIG, + INCENTIVES, INCENTIVE_COUNTER, LP_WEIGHTS_HISTORY, }; use crate::ContractError; @@ -124,7 +124,8 @@ fn create_incentive( ); // the incentive does not exist, all good, continue - // calculates the emission rate + // calculates the emission rate. The way it's calculated, it makes the last epoch to be + // non-inclusive, i.e. the last epoch is not counted in the emission let emission_rate = params .incentive_asset .amount @@ -295,7 +296,7 @@ pub(crate) fn on_epoch_changed( ); // get all LP tokens and update the LP_WEIGHTS_HISTORY - let lp_assets = deps + let lp_denoms = deps .querier .query_all_balances(env.contract.address)? .into_iter() @@ -307,14 +308,29 @@ pub(crate) fn on_epoch_changed( false } }) - .collect::>(); - - for lp_asset in &lp_assets { - LP_WEIGHTS_HISTORY.save( - deps.storage, - (&lp_asset.denom, msg.current_epoch.id), - &lp_asset.amount, - )?; + .map(|asset| asset.denom) + .collect::>(); + + for lp_denom in &lp_denoms { + let lp_weight_option = + LP_WEIGHTS_HISTORY.may_load(deps.storage, (lp_denom, msg.current_epoch.id))?; + + // if the weight for this LP token at this epoch has already been recorded, i.e. someone + // opened or closed positions in the previous epoch, skip it + if lp_weight_option.is_some() { + continue; + } else { + // if the weight for this LP token at this epoch has not been recorded, i.e. no one + // opened or closed positions in the previous epoch, get the last recorded weight + let (_, latest_lp_weight_record) = + get_latest_lp_weight_record(deps.storage, lp_denom, msg.current_epoch.id)?; + + LP_WEIGHTS_HISTORY.save( + deps.storage, + (lp_denom, msg.current_epoch.id), + &latest_lp_weight_record, + )?; + } } Ok(Response::default().add_attributes(vec![ diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index ae66b3962..2c7e170a5 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -39,25 +39,32 @@ pub(crate) fn fill_position( .unwrap_or_else(|| info.clone()); // check if there's an existing open position with the given `identifier` - let mut position = get_position(deps.storage, identifier)?; + let mut position = get_position(deps.storage, identifier.clone())?; if let Some(ref mut position) = position { - // there is a position, fill it + // there is a position, refill it ensure!( position.lp_asset.denom == lp_asset.denom, ContractError::AssetMismatch ); + // if the position is found, ignore if there's a change in the unlocking_duration as it is + // considered the same position, so use the existing unlocking_duration and only update the + // amount of the LP asset + position.lp_asset.amount = position.lp_asset.amount.checked_add(lp_asset.amount)?; POSITIONS.save(deps.storage, &position.identifier, position)?; } else { // No position found, create a new one - let identifier = POSITION_ID_COUNTER + let position_id_counter = POSITION_ID_COUNTER .may_load(deps.storage)? .unwrap_or_default() + 1u64; - POSITION_ID_COUNTER.save(deps.storage, &identifier)?; + POSITION_ID_COUNTER.save(deps.storage, &position_id_counter)?; + + // if no identifier was provided, use the counter as the identifier + let identifier = identifier.unwrap_or(position_id_counter.to_string()); POSITIONS.save( deps.storage, diff --git a/contracts/liquidity_hub/incentive-manager/src/queries.rs b/contracts/liquidity_hub/incentive-manager/src/queries.rs index dd267b4ee..e23e59efd 100644 --- a/contracts/liquidity_hub/incentive-manager/src/queries.rs +++ b/contracts/liquidity_hub/incentive-manager/src/queries.rs @@ -1,14 +1,15 @@ use cosmwasm_std::Deps; -use white_whale_std::coin::aggregate_coins; +use white_whale_std::coin::aggregate_coins; use white_whale_std::incentive_manager::{ - Config, IncentivesBy, IncentivesResponse, PositionsResponse, RewardsResponse, + Config, EpochId, IncentivesBy, IncentivesResponse, LpWeightResponse, PositionsResponse, + RewardsResponse, }; use crate::incentive::commands::calculate_rewards; use crate::state::{ get_incentive_by_identifier, get_incentives, get_incentives_by_incentive_asset, - get_incentives_by_lp_denom, get_positions_by_receiver, CONFIG, + get_incentives_by_lp_denom, get_positions_by_receiver, CONFIG, LP_WEIGHTS_HISTORY, }; use crate::ContractError; @@ -94,3 +95,19 @@ pub(crate) fn query_rewards(deps: Deps, address: String) -> Result Result { + let lp_weight = LP_WEIGHTS_HISTORY + .may_load(deps.storage, (denom.as_str(), epoch_id))? + .ok_or(ContractError::LpWeightNotFound { epoch_id })?; + + Ok(LpWeightResponse { + lp_weight, + epoch_id, + }) +} diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 698cefb8d..2b13f6281 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -243,3 +243,25 @@ pub fn get_latest_address_lp_weight( Err(std_err) => Err(std_err.into()), } } + +/// Gets the latest entry of the LP_WEIGHT_HISTORY for the given lp denom. +/// If there's no LP weight history for the given lp denom, i.e. nobody opened a position ever before, +/// it returns 0 for the weight. +pub fn get_latest_lp_weight_record( + storage: &dyn Storage, + lp_denom: &str, + epoch_id: EpochId, +) -> Result<(EpochId, Uint128), ContractError> { + let latest_weight_history_result = LP_WEIGHTS_HISTORY + .prefix(lp_denom) + .range(storage, None, None, Order::Descending) + .next() + .transpose(); + + match latest_weight_history_result { + Ok(Some(item)) => Ok(item), + // if the lp weight was not found in the map, it returns 0 for the weight. + Ok(None) => Ok((epoch_id, Uint128::zero())), + Err(std_err) => Err(std_err.into()), + } +} diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index 526c7ca35..95c0eb212 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -8,8 +8,8 @@ use cw_multi_test::{ use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig, EpochResponse}; use white_whale_std::incentive_manager::{ - Config, IncentiveAction, IncentivesBy, IncentivesResponse, InstantiateMsg, PositionAction, - PositionsResponse, RewardsResponse, + Config, IncentiveAction, IncentivesBy, IncentivesResponse, InstantiateMsg, LpWeightResponse, + PositionAction, PositionsResponse, RewardsResponse, }; use white_whale_std::pool_network::asset::{Asset, AssetInfo, PairType}; use white_whale_std::pool_network::pair::ExecuteMsg::ProvideLiquidity; @@ -54,6 +54,13 @@ impl TestingSuite { block_info.time = timestamp; self.app.set_block(block_info); + self + } + pub(crate) fn add_one_day(&mut self) -> &mut Self { + let mut block_info = self.app.block_info(); + block_info.time = block_info.time.plus_days(1); + self.app.set_block(block_info); + self } } @@ -468,6 +475,26 @@ impl TestingSuite { self } + #[track_caller] + pub(crate) fn query_lp_weight( + &mut self, + denom: &str, + epoch_id: u64, + result: impl Fn(StdResult), + ) -> &mut Self { + let rewards_response: StdResult = self.app.wrap().query_wasm_smart( + &self.incentive_manager_addr, + &white_whale_std::incentive_manager::QueryMsg::LPWeight { + denom: denom.to_string(), + epoch_id, + }, + ); + + result(rewards_response); + + self + } + #[track_caller] pub(crate) fn query_balance( &mut self, diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index 312124d48..7e19309b2 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -3,7 +3,10 @@ extern crate core; use cosmwasm_std::{coin, Addr, Coin, Decimal, Uint128}; use incentive_manager::ContractError; -use white_whale_std::incentive_manager::{Config, IncentiveAction, IncentiveParams, IncentivesBy}; +use white_whale_std::incentive_manager::{ + Config, IncentiveAction, IncentiveParams, IncentivesBy, LpWeightResponse, Position, + PositionAction, RewardsResponse, +}; use crate::common::suite::TestingSuite; use crate::common::MOCK_CONTRACT_ADDR; @@ -1081,3 +1084,568 @@ pub fn update_config() { assert_eq!(config, expected_config); }); } + +#[test] +pub fn test_manage_position() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + coin(1_000_000_000u128, "invalid_lp".clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let another = suite.senders[2].clone(); + + suite.instantiate_default(); + + let incentive_manager = suite.incentive_manager_addr.clone(); + + suite + .add_hook(creator.clone(), incentive_manager, vec![], |result| { + result.unwrap(); + }) + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(12), + preliminary_end_epoch: Some(16), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(8_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(8_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 10, |result| { + let err = result.unwrap_err().to_string(); + + assert_eq!( + err, + "Generic error: Querier contract error: There's no snapshot of the LP \ + weight in the contract for the epoch 10" + ); + }) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: Some("creator_position".to_string()), + unlocking_duration: 80_400, + receiver: None, + }, + vec![coin(1_000, lp_denom.clone())], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidUnlockingDuration { .. } => {} + _ => panic!( + "Wrong error type, should return ContractError::InvalidUnlockingDuration" + ), + } + }, + ) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: Some("creator_position".to_string()), + unlocking_duration: 32_536_000, + receiver: None, + }, + vec![coin(1_000, lp_denom.clone())], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidUnlockingDuration { .. } => {} + _ => panic!( + "Wrong error type, should return ContractError::InvalidUnlockingDuration" + ), + } + }, + ) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: Some("creator_position".to_string()), + unlocking_duration: 32_536_000, + receiver: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }, + ) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: Some("creator_position".to_string()), + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(1_000, lp_denom.clone())], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 11, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + lp_weight: Uint128::new(1_000), + epoch_id: 11 + } + ); + }) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: Some("creator_position".to_string()), + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(1_000, "invalid_lp".to_string())], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .query_positions(creator.clone(), Some(true), |result| { + let positions = result.unwrap(); + assert_eq!(positions.positions.len(), 1); + assert_eq!( + positions.positions[0], + Position { + identifier: "creator_position".to_string(), + lp_asset: Coin { + denom: "factory/pool/uLP".to_string(), + amount: Uint128::new(1_000) + }, + unlocking_duration: 86400, + open: true, + expiring_at: None, + receiver: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3") + } + ); + }) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: Some("creator_position".to_string()), + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(5_000, lp_denom.clone())], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 11, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + lp_weight: Uint128::new(6_000), + epoch_id: 11 + } + ); + }) + .query_positions(creator.clone(), Some(true), |result| { + let positions = result.unwrap(); + assert_eq!(positions.positions.len(), 1); + assert_eq!( + positions.positions[0], + Position { + identifier: "creator_position".to_string(), + lp_asset: Coin { + denom: "factory/pool/uLP".to_string(), + amount: Uint128::new(6_000) + }, + unlocking_duration: 86400, + open: true, + expiring_at: None, + receiver: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3") + } + ); + }) + .query_lp_weight(&lp_denom, 11, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + lp_weight: Uint128::new(6_000), + epoch_id: 11 + } + ); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 11); + }); + + // make sure snapshots are working correctly + suite + .query_lp_weight(&lp_denom, 15, |result| { + let err = result.unwrap_err().to_string(); + + assert_eq!( + err, + "Generic error: Querier contract error: There's no snapshot of the LP weight in the \ + contract for the epoch 15" + ); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 12); + }) + .query_lp_weight(&lp_denom, 12, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + lp_weight: Uint128::new(6_000), //snapshot taken from the previous epoch + epoch_id: 12 + } + ); + }) + .manage_position( + creator.clone(), + PositionAction::Fill { + //refill position + identifier: Some("creator_position".to_string()), + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(1_000, lp_denom.clone())], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 12, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + // should be the same for epoch 12, as the weight for new positions is added + // to the next epoch + lp_weight: Uint128::new(6_000), + epoch_id: 12 + } + ); + }); + + suite.query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 12); + }); + + suite + .manage_position( + creator.clone(), + PositionAction::Close { + identifier: "creator_position".to_string(), + lp_asset: Some(Coin { + denom: lp_denom.clone(), + amount: Uint128::new(4_000), + }), + }, + vec![coin(4_000, lp_denom.clone())], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }, + ) + .manage_position( + creator.clone(), + PositionAction::Close { + // remove 4_000 from the 7_000 position + identifier: "creator_position".to_string(), + lp_asset: Some(Coin { + denom: lp_denom.clone(), + amount: Uint128::new(4_000), + }), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PendingRewards { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PendingRewards"), + } + }, + ) + .claim( + creator.clone(), + vec![coin(4_000, lp_denom.clone())], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }, + ) + .claim(other.clone(), vec![], |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::NoOpenPositions { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NoOpenPositions"), + } + }) + .query_balance("ulab".to_string(), creator.clone(), |balance| { + assert_eq!(balance, Uint128::new(999_992_000)); + }) + .claim(creator.clone(), vec![], |result| { + result.unwrap(); + }) + .query_balance("ulab".to_string(), creator.clone(), |balance| { + assert_eq!(balance, Uint128::new(999_994_000)); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 1); + assert_eq!( + incentives_response.incentives[0].claimed_amount, + Uint128::new(2_000), + ); + }) + .manage_position( + creator.clone(), + PositionAction::Close { + identifier: "non_existent__position".to_string(), + lp_asset: Some(Coin { + denom: lp_denom.clone(), + amount: Uint128::new(4_000), + }), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::NoPositionFound { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NoPositionFound"), + } + }, + ) + .manage_position( + other.clone(), + PositionAction::Close { + identifier: "creator_position".to_string(), + lp_asset: Some(Coin { + denom: lp_denom.clone(), + amount: Uint128::new(4_000), + }), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }, + ) + .manage_position( + creator.clone(), + PositionAction::Close { + identifier: "creator_position".to_string(), + lp_asset: Some(Coin { + denom: "invalid_lp".to_string(), + amount: Uint128::new(4_000), + }), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .manage_position( + creator.clone(), // someone tries to close the creator's position + PositionAction::Close { + identifier: "creator_position".to_string(), + lp_asset: Some(Coin { + denom: lp_denom.to_string(), + amount: Uint128::new(10_000), + }), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .manage_position( + creator.clone(), + PositionAction::Close { + // remove 5_000 from the 7_000 position + identifier: "creator_position".to_string(), + lp_asset: Some(Coin { + denom: lp_denom.clone(), + amount: Uint128::new(5_000), + }), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 12, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + // should be the same for epoch 12, as the weight for new positions is added + // to the next epoch + lp_weight: Uint128::new(6_000), + epoch_id: 12 + } + ); + }) + .query_lp_weight(&lp_denom, 13, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + // should be the same for epoch 12, as the weight for new positions is added + // to the next epoch + lp_weight: Uint128::new(5_000), + epoch_id: 13 + } + ); + }) + // create a few epochs without any changes in the weight + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .query_lp_weight(&lp_denom, 14, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + // should be the same for epoch 13, as nobody changed their positions + lp_weight: Uint128::new(5_000), + epoch_id: 14 + } + ); + }) + .query_lp_weight(&lp_denom, 15, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + // should be the same for epoch 13, as nobody changed their positions + lp_weight: Uint128::new(5_000), + epoch_id: 15 + } + ); + }) + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 15); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .query_rewards(creator.clone(), |result| { + let rewards_response = result.unwrap(); + match rewards_response { + RewardsResponse::RewardsResponse { rewards } => { + assert_eq!(rewards.len(), 1); + assert_eq!( + rewards[0], + Coin { + denom: "ulab".to_string(), + amount: Uint128::new(6_000) + } + ); + } + RewardsResponse::ClaimRewards { .. } => { + panic!("shouldn't return this but RewardsResponse") + } + } + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0].claimed_amount, + Uint128::new(2_000) + ); + }) + .claim(creator.clone(), vec![], |result| { + result.unwrap(); + }) + .query_balance("ulab".to_string(), creator.clone(), |balance| { + assert_eq!(balance, Uint128::new(1000_000_000)); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0].incentive_asset.amount, + incentives_response.incentives[0].claimed_amount + ); + assert!(incentives_response.incentives[0].is_expired(15)); + }) + .query_rewards(creator.clone(), |result| { + let rewards_response = result.unwrap(); + match rewards_response { + RewardsResponse::RewardsResponse { rewards } => { + assert!(rewards.is_empty()); + } + RewardsResponse::ClaimRewards { .. } => { + panic!("shouldn't return this but RewardsResponse") + } + } + }) + .claim(creator.clone(), vec![], |result| { + result.unwrap(); + }) + .query_balance("ulab".to_string(), creator.clone(), |balance| { + assert_eq!(balance, Uint128::new(1000_000_000)); + }); +} diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index 974961f3e..24df701cd 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -105,6 +105,14 @@ pub enum QueryMsg { /// The address to get all the incentive rewards for. address: String, }, + /// Retrieves the total LP weight in the contract for a given denom on a given epoch. + #[returns(LpWeightResponse)] + LPWeight { + /// The denom to get the total LP weight for. + denom: String, + /// The epoch id to get the LP weight for. + epoch_id: EpochId, + }, } /// Enum to filter incentives by identifier, lp denom or the incentive asset. Used in the Incentives query. @@ -303,3 +311,12 @@ pub struct PositionsResponse { /// All the positions a user has. pub positions: Vec, } + +/// The response for the LP weight query +#[cw_serde] +pub struct LpWeightResponse { + /// The total lp weight in the contract + pub lp_weight: Uint128, + /// The epoch id corresponding to the lp weight in the contract + pub epoch_id: EpochId, +} From 2cb67f4a2426aed316fb842576622d453ea61b17 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 9 Apr 2024 17:28:42 +0100 Subject: [PATCH 28/35] test: add missing tests --- .../incentive-manager/src/helpers.rs | 1 + .../incentive-manager/src/manager/commands.rs | 30 +- .../src/position/commands.rs | 20 +- .../incentive-manager/tests/common/suite.rs | 53 +- .../incentive-manager/tests/integration.rs | 1048 ++++++++++++++--- 5 files changed, 956 insertions(+), 196 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 8f22275c0..b53ecd5df 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -53,6 +53,7 @@ pub(crate) fn process_incentive_creation_fee( ); } else { let refund_amount = paid_fee_amount.saturating_sub(incentive_creation_fee.amount); + if refund_amount > Uint128::zero() { messages.push( BankMsg::Send { diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 1cccaa09f..d8eab8102 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -147,20 +147,22 @@ fn create_incentive( INCENTIVES.save(deps.storage, &incentive.identifier, &incentive)?; - Ok(Response::default().add_attributes(vec![ - ("action", "create_incentive".to_string()), - ("incentive_creator", incentive.owner.to_string()), - ("incentive_identifier", incentive.identifier), - ("start_epoch", incentive.start_epoch.to_string()), - ( - "preliminary_end_epoch", - incentive.preliminary_end_epoch.to_string(), - ), - ("emission_rate", emission_rate.to_string()), - ("curve", incentive.curve.to_string()), - ("incentive_asset", incentive.incentive_asset.to_string()), - ("lp_denom", incentive.lp_denom), - ])) + Ok(Response::default() + .add_messages(messages) + .add_attributes(vec![ + ("action", "create_incentive".to_string()), + ("incentive_creator", incentive.owner.to_string()), + ("incentive_identifier", incentive.identifier), + ("start_epoch", incentive.start_epoch.to_string()), + ( + "preliminary_end_epoch", + incentive.preliminary_end_epoch.to_string(), + ), + ("emission_rate", emission_rate.to_string()), + ("curve", incentive.curve.to_string()), + ("incentive_asset", incentive.incentive_asset.to_string()), + ("lp_denom", incentive.lp_denom), + ])) } /// Closes an incentive. If the incentive has expired, anyone can close it. Otherwise, only the diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 2c7e170a5..7ab90b8d9 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -68,9 +68,9 @@ pub(crate) fn fill_position( POSITIONS.save( deps.storage, - &identifier.to_string(), + &identifier, &Position { - identifier: identifier.to_string(), + identifier: identifier.clone(), lp_asset: lp_asset.clone(), unlocking_duration, open: true, @@ -132,6 +132,12 @@ pub(crate) fn close_position( ("identifier", identifier.to_string()), ]; + let expires_at = env + .block + .time + .plus_seconds(position.unlocking_duration) + .seconds(); + // check if it's going to be closed in full or partially if let Some(lp_asset) = lp_asset { // close position partially @@ -146,12 +152,6 @@ pub(crate) fn close_position( position.lp_asset.amount = position.lp_asset.amount.saturating_sub(lp_asset.amount); // add the partial closing position to the storage - let expires_at = env - .block - .time - .plus_seconds(position.unlocking_duration) - .seconds(); - let identifier = POSITION_ID_COUNTER .may_load(deps.storage)? .unwrap_or_default() @@ -166,10 +166,12 @@ pub(crate) fn close_position( expiring_at: Some(expires_at), receiver: position.receiver.clone(), }; + POSITIONS.save(deps.storage, &identifier.to_string(), &partial_position)?; } else { // close position in full position.open = false; + position.expiring_at = Some(expires_at); } let close_in_full = !position.open; attributes.push(("close_in_full", close_in_full.to_string())); @@ -243,7 +245,7 @@ pub(crate) fn withdraw_position( return Err(ContractError::Unauthorized); } - if position.expiring_at.unwrap() < env.block.time.seconds() { + if position.expiring_at.unwrap() > env.block.time.seconds() { return Err(ContractError::PositionNotExpired); } } diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index 95c0eb212..df8ec757d 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -7,6 +7,7 @@ use cw_multi_test::{ }; use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig, EpochResponse}; +use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; use white_whale_std::incentive_manager::{ Config, IncentiveAction, IncentivesBy, IncentivesResponse, InstantiateMsg, LpWeightResponse, PositionAction, PositionsResponse, RewardsResponse, @@ -61,6 +62,13 @@ impl TestingSuite { block_info.time = block_info.time.plus_days(1); self.app.set_block(block_info); + self + } + pub(crate) fn add_half_a_day(&mut self) -> &mut Self { + let mut block_info = self.app.block_info(); + block_info.time = block_info.time.plus_hours(12); + self.app.set_block(block_info); + self } } @@ -385,6 +393,31 @@ impl TestingSuite { self } + + #[track_caller] + pub(crate) fn on_epoch_changed( + &mut self, + sender: Addr, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = + white_whale_std::incentive_manager::ExecuteMsg::EpochChangedHook(EpochChangedHookMsg { + current_epoch: Epoch { + id: 0, + start_time: Default::default(), + }, + }); + + result(self.app.execute_contract( + sender, + self.incentive_manager_addr.clone(), + &msg, + &funds, + )); + + self + } } /// queries @@ -547,26 +580,6 @@ impl TestingSuite { self } - #[track_caller] - pub(crate) fn remove_hook( - &mut self, - sender: Addr, - contract_addr: Addr, - funds: Vec, - result: impl Fn(Result), - ) -> &mut Self { - let msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::RemoveHook { - contract_addr: contract_addr.to_string(), - }; - - result( - self.app - .execute_contract(sender, self.epoch_manager_addr.clone(), &msg, &funds), - ); - - self - } - #[track_caller] pub(crate) fn query_current_epoch( &mut self, diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index 7e19309b2..15d515328 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -1,11 +1,13 @@ extern crate core; +use std::cell::RefCell; + use cosmwasm_std::{coin, Addr, Coin, Decimal, Uint128}; use incentive_manager::ContractError; use white_whale_std::incentive_manager::{ - Config, IncentiveAction, IncentiveParams, IncentivesBy, LpWeightResponse, Position, - PositionAction, RewardsResponse, + Config, Curve, EpochId, Incentive, IncentiveAction, IncentiveParams, IncentivesBy, + LpWeightResponse, Position, PositionAction, RewardsResponse, }; use crate::common::suite::TestingSuite; @@ -465,7 +467,7 @@ fn create_incentives() { incentives_response.incentives[0].incentive_asset, Coin { denom: "ulab".to_string(), - amount: Uint128::new(4_000) + amount: Uint128::new(4_000), } ); }, @@ -481,7 +483,7 @@ fn create_incentives() { incentives_response.incentives[0].incentive_asset, Coin { denom: "ulab".to_string(), - amount: Uint128::new(10_000) + amount: Uint128::new(10_000), } ); }, @@ -630,7 +632,7 @@ fn expand_incentives() { incentive.incentive_asset, Coin { denom: "ulab".to_string(), - amount: Uint128::new(4_000) + amount: Uint128::new(4_000), } ); @@ -668,7 +670,7 @@ fn expand_incentives() { incentive.incentive_asset, Coin { denom: "ulab".to_string(), - amount: Uint128::new(9_000) + amount: Uint128::new(9_000), } ); @@ -676,6 +678,7 @@ fn expand_incentives() { }, ); } + #[test] fn close_incentives() { let lp_denom = "factory/pool/uLP".to_string(); @@ -917,153 +920,153 @@ pub fn update_config() { }; suite.query_config(|result| { - let config = result.unwrap(); - assert_eq!(config, expected_config); - }) + let config = result.unwrap(); + assert_eq!(config, expected_config); + }) .update_config( other.clone(), Some(MOCK_CONTRACT_ADDR.to_string()), Some(MOCK_CONTRACT_ADDR.to_string()), - Some(Coin { - denom: "uwhale".to_string(), - amount: Uint128::new(2_000u128), - }), - Some(3u32), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(3u32), Some(15u32), - Some(172_800u64), - Some(864_000u64), - Some(Decimal::percent(50)), + Some(172_800u64), + Some(864_000u64), + Some(Decimal::percent(50)), vec![coin(1_000, "uwhale")], - |result|{ + |result| { let err = result.unwrap_err().downcast::().unwrap(); match err { ContractError::PaymentError { .. } => {} _ => panic!("Wrong error type, should return ContractError::PaymentError"), } - } - ) .update_config( - other.clone(), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(Coin { - denom: "uwhale".to_string(), - amount: Uint128::new(2_000u128), - }), - Some(0u32), - Some(15u32), - Some(172_800u64), - Some(864_000u64), - Some(Decimal::percent(50)), - vec![], - |result|{ - let err = result.unwrap_err().downcast::().unwrap(); - match err { - ContractError::OwnershipError { .. } => {} - _ => panic!("Wrong error type, should return ContractError::OwnershipError"), - } - } + }, ).update_config( - creator.clone(), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(Coin { - denom: "uwhale".to_string(), - amount: Uint128::new(2_000u128), - }), - Some(0u32), - Some(15u32), - Some(172_800u64), - Some(864_000u64), - Some(Decimal::percent(50)), - vec![], - |result|{ - let err = result.unwrap_err().downcast::().unwrap(); - match err { - ContractError::UnspecifiedConcurrentIncentives { .. } => {} - _ => panic!("Wrong error type, should return ContractError::UnspecifiedConcurrentIncentives"), - } + other.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(0u32), + Some(15u32), + Some(172_800u64), + Some(864_000u64), + Some(Decimal::percent(50)), + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::OwnershipError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::OwnershipError"), } - ).update_config( - creator.clone(), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(Coin { - denom: "uwhale".to_string(), - amount: Uint128::new(2_000u128), - }), - Some(5u32), - Some(15u32), - Some(80_800u64), - Some(80_000u64), - Some(Decimal::percent(50)), - vec![], - |result|{ - let err = result.unwrap_err().downcast::().unwrap(); - match err { - ContractError::InvalidUnbondingRange { .. } => {} - _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), - } + }, + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(0u32), + Some(15u32), + Some(172_800u64), + Some(864_000u64), + Some(Decimal::percent(50)), + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::UnspecifiedConcurrentIncentives { .. } => {} + _ => panic!("Wrong error type, should return ContractError::UnspecifiedConcurrentIncentives"), } - ).update_config( - creator.clone(), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(Coin { - denom: "uwhale".to_string(), - amount: Uint128::new(2_000u128), - }), - Some(5u32), - Some(15u32), - Some(300_000u64), - Some(200_000u64), - Some(Decimal::percent(50)), - vec![], - |result|{ - let err = result.unwrap_err().downcast::().unwrap(); - match err { - ContractError::InvalidUnbondingRange { .. } => {} - _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), - } + }, + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(80_800u64), + Some(80_000u64), + Some(Decimal::percent(50)), + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidUnbondingRange { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), } - ).update_config( - creator.clone(), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(Coin { - denom: "uwhale".to_string(), - amount: Uint128::new(2_000u128), - }), - Some(5u32), - Some(15u32), - Some(100_000u64), - Some(200_000u64), - Some(Decimal::percent(105)), - vec![], - |result|{ - let err = result.unwrap_err().downcast::().unwrap(); - match err { - ContractError::InvalidEmergencyUnlockPenalty { .. } => {} - _ => panic!("Wrong error type, should return ContractError::InvalidEmergencyUnlockPenalty"), - } + }, + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(300_000u64), + Some(200_000u64), + Some(Decimal::percent(50)), + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidUnbondingRange { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), } - ).update_config( - creator.clone(), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(MOCK_CONTRACT_ADDR.to_string()), - Some(Coin { - denom: "uwhale".to_string(), - amount: Uint128::new(2_000u128), - }), - Some(5u32), - Some(15u32), - Some(100_000u64), - Some(200_000u64), - Some(Decimal::percent(20)), - vec![], - |result|{ - result.unwrap(); + }, + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(100_000u64), + Some(200_000u64), + Some(Decimal::percent(105)), + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidEmergencyUnlockPenalty { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InvalidEmergencyUnlockPenalty"), } - ); + }, + ).update_config( + creator.clone(), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(MOCK_CONTRACT_ADDR.to_string()), + Some(Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2_000u128), + }), + Some(5u32), + Some(15u32), + Some(100_000u64), + Some(200_000u64), + Some(Decimal::percent(20)), + vec![], + |result| { + result.unwrap(); + }, + ); let expected_config = Config { whale_lair_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), @@ -1208,7 +1211,7 @@ pub fn test_manage_position() { lp_weight, LpWeightResponse { lp_weight: Uint128::new(1_000), - epoch_id: 11 + epoch_id: 11, } ); }) @@ -1237,12 +1240,12 @@ pub fn test_manage_position() { identifier: "creator_position".to_string(), lp_asset: Coin { denom: "factory/pool/uLP".to_string(), - amount: Uint128::new(1_000) + amount: Uint128::new(1_000), }, unlocking_duration: 86400, open: true, expiring_at: None, - receiver: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3") + receiver: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), } ); }) @@ -1258,13 +1261,29 @@ pub fn test_manage_position() { result.unwrap(); }, ) + .manage_position( + creator.clone(), + PositionAction::Withdraw { + identifier: "creator_position".to_string(), + emergency_unlock: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + // the position is not closed or hasn't expired yet + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }, + ) .query_lp_weight(&lp_denom, 11, |result| { let lp_weight = result.unwrap(); assert_eq!( lp_weight, LpWeightResponse { lp_weight: Uint128::new(6_000), - epoch_id: 11 + epoch_id: 11, } ); }) @@ -1277,12 +1296,12 @@ pub fn test_manage_position() { identifier: "creator_position".to_string(), lp_asset: Coin { denom: "factory/pool/uLP".to_string(), - amount: Uint128::new(6_000) + amount: Uint128::new(6_000), }, unlocking_duration: 86400, open: true, expiring_at: None, - receiver: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3") + receiver: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), } ); }) @@ -1292,7 +1311,7 @@ pub fn test_manage_position() { lp_weight, LpWeightResponse { lp_weight: Uint128::new(6_000), - epoch_id: 11 + epoch_id: 11, } ); }) @@ -1311,10 +1330,10 @@ pub fn test_manage_position() { let err = result.unwrap_err().to_string(); assert_eq!( - err, - "Generic error: Querier contract error: There's no snapshot of the LP weight in the \ + err, + "Generic error: Querier contract error: There's no snapshot of the LP weight in the \ contract for the epoch 15" - ); + ); }) .add_one_day() .create_epoch(creator.clone(), |result| { @@ -1330,7 +1349,7 @@ pub fn test_manage_position() { lp_weight, LpWeightResponse { lp_weight: Uint128::new(6_000), //snapshot taken from the previous epoch - epoch_id: 12 + epoch_id: 12, } ); }) @@ -1355,7 +1374,7 @@ pub fn test_manage_position() { // should be the same for epoch 12, as the weight for new positions is added // to the next epoch lp_weight: Uint128::new(6_000), - epoch_id: 12 + epoch_id: 12, } ); }); @@ -1525,6 +1544,23 @@ pub fn test_manage_position() { result.unwrap(); }, ) + .manage_position( + creator.clone(), + PositionAction::Withdraw { + identifier: "2".to_string(), + emergency_unlock: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PositionNotExpired { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::PositionNotExpired") + } + } + }, + ) .query_lp_weight(&lp_denom, 12, |result| { let lp_weight = result.unwrap(); assert_eq!( @@ -1533,7 +1569,7 @@ pub fn test_manage_position() { // should be the same for epoch 12, as the weight for new positions is added // to the next epoch lp_weight: Uint128::new(6_000), - epoch_id: 12 + epoch_id: 12, } ); }) @@ -1545,7 +1581,7 @@ pub fn test_manage_position() { // should be the same for epoch 12, as the weight for new positions is added // to the next epoch lp_weight: Uint128::new(5_000), - epoch_id: 13 + epoch_id: 13, } ); }) @@ -1554,6 +1590,52 @@ pub fn test_manage_position() { .create_epoch(creator.clone(), |result| { result.unwrap(); }) + //after a day the closed position should be able to be withdrawn + .manage_position( + other.clone(), + PositionAction::Withdraw { + identifier: "creator_position".to_string(), + emergency_unlock: None, + }, + vec![coin(5_000, lp_denom.clone())], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }, + ) + .manage_position( + creator.clone(), + PositionAction::Withdraw { + identifier: "non_existent_position".to_string(), + emergency_unlock: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::NoPositionFound { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NoPositionFound"), + } + }, + ) + .manage_position( + other.clone(), + PositionAction::Withdraw { + identifier: "2".to_string(), + emergency_unlock: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }, + ) .add_one_day() .create_epoch(creator.clone(), |result| { result.unwrap(); @@ -1569,7 +1651,7 @@ pub fn test_manage_position() { LpWeightResponse { // should be the same for epoch 13, as nobody changed their positions lp_weight: Uint128::new(5_000), - epoch_id: 14 + epoch_id: 14, } ); }) @@ -1580,7 +1662,7 @@ pub fn test_manage_position() { LpWeightResponse { // should be the same for epoch 13, as nobody changed their positions lp_weight: Uint128::new(5_000), - epoch_id: 15 + epoch_id: 15, } ); }) @@ -1601,7 +1683,7 @@ pub fn test_manage_position() { rewards[0], Coin { denom: "ulab".to_string(), - amount: Uint128::new(6_000) + amount: Uint128::new(6_000), } ); } @@ -1647,5 +1729,665 @@ pub fn test_manage_position() { }) .query_balance("ulab".to_string(), creator.clone(), |balance| { assert_eq!(balance, Uint128::new(1000_000_000)); + }) + .manage_position( + creator.clone(), + PositionAction::Withdraw { + identifier: "2".to_string(), + emergency_unlock: None, + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_positions(other.clone(), Some(false), |result| { + let positions = result.unwrap(); + assert!(positions.positions.is_empty()); + }) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: None, + unlocking_duration: 86_400, + receiver: Some(another.clone().to_string()), + }, + vec![coin(5_000, lp_denom.clone())], + |result| { + result.unwrap(); + }, + ) + .query_positions(another.clone(), Some(true), |result| { + let positions = result.unwrap(); + assert_eq!(positions.positions.len(), 1); + assert_eq!( + positions.positions[0], + Position { + identifier: "3".to_string(), + lp_asset: Coin { + denom: "factory/pool/uLP".to_string(), + amount: Uint128::new(5_000), + }, + unlocking_duration: 86400, + open: true, + expiring_at: None, + receiver: another.clone(), + } + ); + }) + .manage_position( + creator.clone(), + PositionAction::Close { + identifier: "3".to_string(), + lp_asset: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }, + ) + .manage_position( + another.clone(), + PositionAction::Close { + identifier: "3".to_string(), + lp_asset: None, //close in full + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_positions(another.clone(), Some(true), |result| { + let positions = result.unwrap(); + assert!(positions.positions.is_empty()); + }) + .query_positions(another.clone(), Some(false), |result| { + let positions = result.unwrap(); + assert_eq!(positions.positions.len(), 1); + assert_eq!( + positions.positions[0], + Position { + identifier: "3".to_string(), + lp_asset: Coin { + denom: "factory/pool/uLP".to_string(), + amount: Uint128::new(5_000), + }, + unlocking_duration: 86400, + open: false, + expiring_at: Some(1712847600), + receiver: another.clone(), + } + ); }); } + +#[test] +fn claim_expired_incentive_returns_nothing() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + coin(1_000_000_000u128, "invalid_lp".clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + + suite.instantiate_default(); + + let incentive_manager = suite.incentive_manager_addr.clone(); + + suite + .add_hook(creator.clone(), incentive_manager, vec![], |result| { + result.unwrap(); + }) + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(12), + preliminary_end_epoch: Some(16), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(8_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(8_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_position( + other.clone(), + PositionAction::Fill { + identifier: Some("creator_position".to_string()), + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(5_000, lp_denom.clone())], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 11, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + lp_weight: Uint128::new(5_000), + epoch_id: 11, + } + ); + }) + .query_positions(other.clone(), Some(true), |result| { + let positions = result.unwrap(); + assert_eq!(positions.positions.len(), 1); + assert_eq!( + positions.positions[0], + Position { + identifier: "creator_position".to_string(), + lp_asset: Coin { + denom: "factory/pool/uLP".to_string(), + amount: Uint128::new(5_000), + }, + unlocking_duration: 86400, + open: true, + expiring_at: None, + receiver: Addr::unchecked("migaloo193lk767456jhkzddnz7kf5jvuzfn67gyfvhc40"), + } + ); + }); + + // create a couple of epochs to make the incentive active + + suite + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .add_one_day() + .create_epoch(creator.clone(), |result| { + result.unwrap(); + }) + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 14); + }) + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(1_000_000_000u128)); + }) + .claim(other.clone(), vec![], |result| { + result.unwrap(); + }) + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(1_000_006_000u128)); + }); + + // create a bunch of epochs to make the incentive expire + for _ in 0..15 { + suite.add_one_day().create_epoch(creator.clone(), |result| { + result.unwrap(); + }); + } + + // there shouldn't be anything to claim as the incentive has expired, even though it still has some funds + suite + .query_rewards(creator.clone(), |result| { + let rewards_response = result.unwrap(); + match rewards_response { + RewardsResponse::RewardsResponse { rewards } => { + assert!(rewards.is_empty()); + } + RewardsResponse::ClaimRewards { .. } => { + panic!("shouldn't return this but RewardsResponse") + } + } + }) + .claim(other.clone(), vec![], |result| { + result.unwrap(); + }) + .query_balance("ulab".to_string(), other.clone(), |balance| { + // the balance hasn't changed + assert_eq!(balance, Uint128::new(1_000_006_000u128)); + }); +} + +#[test] +fn test_close_expired_incentives() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + coin(1_000_000_000u128, "invalid_lp".clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + + suite.instantiate_default(); + + let incentive_manager = suite.incentive_manager_addr.clone(); + + suite + .add_hook(creator.clone(), incentive_manager, vec![], |result| { + result.unwrap(); + }) + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(12), + preliminary_end_epoch: Some(16), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(8_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(8_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ); + + // create a bunch of epochs to make the incentive expire + for _ in 0..20 { + suite.add_one_day().create_epoch(creator.clone(), |result| { + result.unwrap(); + }); + } + + let current_id: RefCell = RefCell::new(0u64); + + // try opening another incentive for the same lp denom, the expired incentive should get closed + suite + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + *current_id.borrow_mut() = epoch_response.epoch.id; + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 1); + assert!(incentives_response.incentives[0].is_expired(current_id.borrow().clone())); + }) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: None, + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(10_000u128), + }, + incentive_identifier: Some("new_incentive".to_string()), + }, + }, + vec![coin(10_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!(incentives_response.incentives.len(), 1); + assert_eq!( + incentives_response.incentives[0], + Incentive { + identifier: "new_incentive".to_string(), + owner: other.clone(), + lp_denom: lp_denom.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(10_000u128), + }, + claimed_amount: Uint128::zero(), + emission_rate: Uint128::new(714), + curve: Curve::Linear, + start_epoch: 30u64, + preliminary_end_epoch: 44u64, + last_epoch_claimed: 29u64, + } + ); + }); +} + +#[test] +fn on_epoch_changed_unauthorized() { + let mut suite = TestingSuite::default_with_balances(vec![]); + let creator = suite.creator(); + + suite + .instantiate_default() + .on_epoch_changed(creator, vec![], |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }); +} + +#[test] +fn expand_expired_incentive() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + + suite.instantiate_default(); + + suite.manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: None, + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: Some("incentive".to_string()), + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ); + + // create a bunch of epochs to make the incentive expire + for _ in 0..15 { + suite.add_one_day().create_epoch(creator.clone(), |result| { + result.unwrap(); + }); + } + + suite.manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: None, + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(8_000u128), + }, + incentive_identifier: Some("incentive".to_string()), + }, + }, + vec![coin(8_000u128, "ulab")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::IncentiveAlreadyExpired { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::IncentiveAlreadyExpired") + } + } + }, + ); +} + +#[test] +fn test_emergency_withdrawal() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + + suite.instantiate_default(); + + let whale_lair_addr = suite.whale_lair_addr.clone(); + + suite + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: None, + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: Some("incentive".to_string()), + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_position( + other.clone(), + PositionAction::Fill { + identifier: Some("other_position".to_string()), + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(1_000, lp_denom.clone())], + |result| { + result.unwrap(); + }, + ) + .query_positions(other.clone(), Some(true), |result| { + let positions = result.unwrap(); + assert_eq!(positions.positions.len(), 1); + assert_eq!( + positions.positions[0], + Position { + identifier: "other_position".to_string(), + lp_asset: Coin { + denom: "factory/pool/uLP".to_string(), + amount: Uint128::new(1_000), + }, + unlocking_duration: 86400, + open: true, + expiring_at: None, + receiver: other.clone(), + } + ); + }) + .query_balance(lp_denom.clone().to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(999_999_000)); + }) + .query_balance( + lp_denom.clone().to_string(), + whale_lair_addr.clone(), + |balance| { + assert_eq!(balance, Uint128::zero()); + }, + ) + .manage_position( + other.clone(), + PositionAction::Withdraw { + identifier: "other_position".to_string(), + emergency_unlock: Some(true), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_balance(lp_denom.clone().to_string(), other.clone(), |balance| { + //emergency unlock penalty is 10% of the position amount, so the user gets 1000 - 100 = 900 + assert_eq!(balance, Uint128::new(999_999_900)); + }) + .query_balance( + lp_denom.clone().to_string(), + whale_lair_addr.clone(), + |balance| { + assert_eq!(balance, Uint128::new(100)); + }, + ); +} + +#[test] +fn test_incentive_helper() { + let lp_denom = "factory/pool/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + + suite.instantiate_default(); + + let incentive_manager_addr = suite.incentive_manager_addr.clone(); + let whale_lair_addr = suite.whale_lair_addr.clone(); + + suite + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: None, + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: Some("incentive".to_string()), + }, + }, + vec![coin(3_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::AssetMismatch { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::AssetMismatch") + } + } + }, + ) + .query_balance("uwhale".to_string(), creator.clone(), |balance| { + assert_eq!(balance, Uint128::new(1_000_000_000)); + }) + .query_balance("uwhale".to_string(), whale_lair_addr.clone(), |balance| { + assert_eq!(balance, Uint128::zero()); + }) + .query_balance( + "uwhale".to_string(), + incentive_manager_addr.clone(), + |balance| { + assert_eq!(balance, Uint128::zero()); + }, + ) + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: None, + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(2_000u128), + }, + incentive_identifier: Some("incentive".to_string()), + }, + }, + vec![coin(2_000, "ulab"), coin(3_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .query_balance("uwhale".to_string(), whale_lair_addr.clone(), |balance| { + assert_eq!(balance, Uint128::new(1_000)); + }) + .query_balance( + "uwhale".to_string(), + incentive_manager_addr.clone(), + |balance| { + assert_eq!(balance, Uint128::zero()); + }, + ) + .query_balance("uwhale".to_string(), creator.clone(), |balance| { + // got the excess of whale back + assert_eq!(balance, Uint128::new(999_999_000)); + }); + + suite.manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: None, + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(2_000u128), + }, + incentive_identifier: Some("underpaid_incentive".to_string()), + }, + }, + vec![coin(2_000, "ulab"), coin(500, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::IncentiveFeeNotPaid { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::IncentiveFeeNotPaid") + } + } + }, + ); +} From ff5763e1e478be4270e745b5aa0ce95c3fff9d50 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 10 Apr 2024 13:51:09 +0100 Subject: [PATCH 29/35] test: add complex test --- .../src/incentive/commands.rs | 11 +- .../incentive-manager/src/manager/commands.rs | 2 +- .../incentive-manager/src/position/helpers.rs | 4 +- .../incentive-manager/src/position/mod.rs | 1 + .../src/position/tests/mod.rs | 1 + .../src/position/tests/weight_calculation.rs | 32 +++ .../incentive-manager/src/state.rs | 1 + .../incentive-manager/tests/common/suite.rs | 10 + .../incentive-manager/tests/integration.rs | 260 ++++++++++++++++++ 9 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 contracts/liquidity_hub/incentive-manager/src/position/tests/mod.rs create mode 100644 contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index 7b70046e0..00c14f02b 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -132,11 +132,12 @@ pub(crate) fn calculate_rewards( let mut modified_incentives: HashMap = HashMap::new(); for incentive in incentives { + println!("*********"); // skip expired incentives if incentive.is_expired(current_epoch_id) { continue; } - + println!("Incentive: {:?}", incentive); // compute where the user can start claiming rewards for the incentive let start_from_epoch = compute_start_from_epoch_for_incentive( deps.storage, @@ -160,7 +161,7 @@ pub(crate) fn calculate_rewards( for epoch_id in start_from_epoch..=until_epoch { let user_weight = user_weights[&epoch_id]; let total_lp_weight = LP_WEIGHTS_HISTORY - .may_load(deps.storage, (&position.lp_asset.denom, epoch_id))? + .may_load(deps.storage, (&incentive.lp_denom, epoch_id))? .ok_or(ContractError::LpWeightNotFound { epoch_id })?; let user_share = (user_weight, total_lp_weight); @@ -183,7 +184,11 @@ pub(crate) fn calculate_rewards( amount: reward, }); } - + println!("----"); + println!("epoch_id: {:?}", epoch_id); + println!("total_lp_weight: {:?}", total_lp_weight); + println!("user_share: {:?}", user_share); + println!("Reward: {:?}", reward); if is_claim { // compound the rewards for the incentive let maybe_reward = modified_incentives diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index d8eab8102..0df26310a 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -142,7 +142,7 @@ fn create_incentive( owner: info.sender, claimed_amount: Uint128::zero(), emission_rate, - last_epoch_claimed: current_epoch.id - 1, + last_epoch_claimed: start_epoch - 1, }; INCENTIVES.save(deps.storage, &incentive.identifier, &incentive)?; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index da5293a8b..c10748dc2 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -75,10 +75,10 @@ pub fn get_latest_address_weight( /// Gets the latest available weight snapshot recorded for the given lp. pub fn get_latest_lp_weight( storage: &dyn Storage, - lp_asset: &str, + lp_denom: &str, ) -> Result<(EpochId, Uint128), ContractError> { let result = LP_WEIGHTS_HISTORY - .prefix(lp_asset) + .prefix(lp_denom) .range(storage, None, None, Order::Descending) .take(1usize) // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. diff --git a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs index 39da526a6..a0fd1dab5 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/mod.rs @@ -1,2 +1,3 @@ pub mod commands; mod helpers; +mod tests; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/tests/mod.rs b/contracts/liquidity_hub/incentive-manager/src/position/tests/mod.rs new file mode 100644 index 000000000..c625f2cb6 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/position/tests/mod.rs @@ -0,0 +1 @@ +mod weight_calculation; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs b/contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs new file mode 100644 index 000000000..53861a476 --- /dev/null +++ b/contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs @@ -0,0 +1,32 @@ +#[test] +fn test_calculate_weight() { + use crate::position::helpers::calculate_weight; + use cosmwasm_std::coin; + + let weight = calculate_weight(&coin(100, "uwhale"), 86400u64).unwrap(); + println!("1 day Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 1209600).unwrap(); + println!("2 weeks Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 2629746).unwrap(); + println!("1 month Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 5259492).unwrap(); + println!("2 months Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 7889238).unwrap(); + println!("3 months Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 10518984).unwrap(); + println!("4 months Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 13148730).unwrap(); + println!("5 months Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 15778476).unwrap(); + println!("6 months Weight: {:?}", weight); + + let weight = calculate_weight(&coin(100, "uwhale"), 31556926).unwrap(); + println!("1 year Weight: {:?}", weight); +} diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 2b13f6281..a84edb4fb 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -44,6 +44,7 @@ pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch /// The history of total weight (sum of all individual weights) of an LP asset at a given epoch pub const LP_WEIGHTS_HISTORY: Map<(&str, EpochId), Uint128> = Map::new("lp_weights_history"); +//todo add the lp denom here as well, otherwise there's no way to distinguish /// The address lp weight history, i.e. how much lp weight an address had at a given epoch pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, EpochId), Uint128> = Map::new("address_lp_weight_history"); diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index df8ec757d..7a323160b 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -71,6 +71,16 @@ impl TestingSuite { self } + + pub(crate) fn add_one_epoch(&mut self) -> &mut Self { + let creator = self.creator(); + + self.add_one_day().create_epoch(creator, |res| { + res.unwrap(); + }); + + self + } } /// Instantiate diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index 15d515328..d53882174 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -2391,3 +2391,263 @@ fn test_incentive_helper() { }, ); } + +/// Complex test case with 4 incentives for 2 different LPs somewhat overlapping in time +/// Incentive 1 -> runs from epoch 12 to 16 +/// Incentive 2 -> run from epoch 14 to 25 +/// Incentive 3 -> runs from epoch 20 to 23 +/// Incentive 4 -> runs from epoch 23 to 37 +/// +/// There are 3 users, creator, other and another +/// +/// Locking tokens: +/// creator locks 35% of the LP tokens before incentive 1 starts +/// other locks 40% of the LP tokens before after incentive 1 starts and before incentive 2 starts +/// another locks 25% of the LP tokens after incentive 3 starts, before incentive 3 ends +/// +/// Unlocking tokens: +/// creator never unlocks +/// other emergency unlocks mid-way through incentive 2 +/// another partially unlocks mid-way through incentive 4 +/// +/// Verify users got rewards pro rata to their locked tokens +#[test] +fn test_multiple_incentives_and_positions() { + let lp_denom_1 = "factory/pool1/uLP".to_string(); + let lp_denom_2 = "factory/pool2/uLP".to_string(); + + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "ulab".to_string()), + coin(1_000_000_000u128, "uosmo".to_string()), + coin(1_000_000_000u128, lp_denom_1.clone()), + coin(1_000_000_000u128, lp_denom_2.clone()), + ]); + + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let another = suite.senders[2].clone(); + + suite.instantiate_default(); + + let incentive_manager_addr = suite.incentive_manager_addr.clone(); + let whale_lair_addr = suite.whale_lair_addr.clone(); + + // create 4 incentives with 2 different LPs + suite + .add_hook(creator.clone(), incentive_manager_addr, vec![], |result| { + result.unwrap(); + }) + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 10); + }) + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom_1.clone(), + start_epoch: Some(12), + preliminary_end_epoch: Some(16), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(80_000u128), + }, + incentive_identifier: Some("incentive_1".to_string()), + }, + }, + vec![coin(80_000u128, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_incentive( + creator.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom_1.clone(), + start_epoch: Some(14), + preliminary_end_epoch: Some(25), + curve: None, + incentive_asset: Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(10_000u128), + }, + incentive_identifier: Some("incentive_2".to_string()), + }, + }, + vec![coin(10_000u128, "uosmo"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom_2.clone(), + start_epoch: Some(20), + preliminary_end_epoch: Some(23), + curve: None, + incentive_asset: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(30_000u128), + }, + incentive_identifier: Some("incentive_3".to_string()), + }, + }, + vec![coin(31_000u128, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom_2.clone(), + start_epoch: Some(23), + preliminary_end_epoch: None, + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(70_000u128), + }, + incentive_identifier: Some("incentive_4".to_string()), + }, + }, + vec![coin(70_000u128, "ulab"), coin(1_000, "uwhale")], + |result| { + result.unwrap(); + }, + ); + + // creator fills a position + suite + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: None, + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(35_000, lp_denom_1.clone())], + |result| { + result.unwrap(); + }, + ) + .manage_position( + creator.clone(), + PositionAction::Fill { + identifier: None, + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(35_000, lp_denom_2.clone())], + |result| { + result.unwrap(); + }, + ); + + suite + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 13); + }); + + // other fills a position + suite + .manage_position( + other.clone(), + PositionAction::Fill { + identifier: None, + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(40_000, lp_denom_1.clone())], + |result| { + result.unwrap(); + }, + ) + .manage_position( + other.clone(), + PositionAction::Fill { + identifier: None, + unlocking_duration: 86_400, + receiver: None, + }, + vec![coin(40_000, lp_denom_2.clone())], + |result| { + result.unwrap(); + }, + ); + + suite + .add_one_epoch() + .add_one_epoch() + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 15); + }); + + suite + .query_incentives( + Some(IncentivesBy::Identifier("incentive_1".to_string())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0], + Incentive { + identifier: "incentive_1".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(80_000u128), + }, + claimed_amount: Uint128::zero(), + emission_rate: Uint128::new(20_000), + curve: Curve::Linear, + start_epoch: 12u64, + preliminary_end_epoch: 16u64, + last_epoch_claimed: 11u64, + } + ); + }, + ) + .claim(creator.clone(), vec![], |result| { + result.unwrap(); + }) + .query_incentives( + Some(IncentivesBy::Identifier("incentive_1".to_string())), + None, + None, + |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0], + Incentive { + identifier: "incentive_1".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(80_000u128), + }, + claimed_amount: Uint128::new(58_666), + emission_rate: Uint128::new(20_000), + curve: Curve::Linear, + start_epoch: 12u64, + preliminary_end_epoch: 16u64, + last_epoch_claimed: 15u64, + } + ); + }, + ); +} From 5f4d388b9a1c1c163f357b4722aad5113cbe0e46 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 10 Apr 2024 16:42:09 +0100 Subject: [PATCH 30/35] test: final tweaks and tests complementation --- .../incentive-manager/src/contract.rs | 2 +- .../incentive-manager/src/error.rs | 75 +- .../incentive-manager/src/helpers.rs | 18 +- .../src/incentive/commands.rs | 82 +-- .../src/incentive/tests/helpers.rs | 11 - .../src/incentive/tests/mod.rs | 1 - .../src/incentive/tests/rewards.rs | 84 ++- .../incentive-manager/src/manager/commands.rs | 4 +- .../src/position/commands.rs | 33 +- .../incentive-manager/src/position/helpers.rs | 3 +- .../src/position/tests/weight_calculation.rs | 27 +- .../incentive-manager/src/queries.rs | 2 +- .../incentive-manager/src/state.rs | 8 +- .../incentive-manager/tests/common/suite.rs | 12 +- .../incentive-manager/tests/integration.rs | 664 +++++++++++++++++- 15 files changed, 815 insertions(+), 211 deletions(-) delete mode 100644 contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index c16d88754..c806df944 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -35,7 +35,7 @@ pub fn instantiate( // ensure the unlocking duration range is valid ensure!( msg.max_unlocking_duration > msg.min_unlocking_duration, - ContractError::InvalidUnbondingRange { + ContractError::InvalidUnlockingRange { min: msg.min_unlocking_duration, max: msg.max_unlocking_duration, } diff --git a/contracts/liquidity_hub/incentive-manager/src/error.rs b/contracts/liquidity_hub/incentive-manager/src/error.rs index 40e0f607f..261d38ffa 100644 --- a/contracts/liquidity_hub/incentive-manager/src/error.rs +++ b/contracts/liquidity_hub/incentive-manager/src/error.rs @@ -47,23 +47,9 @@ pub enum ContractError { #[error("max_concurrent_flows cannot be set to zero")] UnspecifiedConcurrentIncentives, - #[error("Invalid unbonding range, specified min as {min} and max as {max}")] - InvalidUnbondingRange { - /// The minimum unbonding time - min: u64, - /// The maximum unbonding time - max: u64, - }, - #[error("Incentive doesn't exist")] NonExistentIncentive {}, - #[error("Attempt to create a new incentive, which exceeds the maximum of {max} incentives allowed per LP at a time")] - TooManyIncentives { - /// The maximum amount of incentives that can exist - max: u32, - }, - #[error("Attempt to create a new incentive with a small incentive_asset amount, which is less than the minimum of {min}")] InvalidIncentiveAmount { /// The minimum amount of an asset to create an incentive with @@ -73,12 +59,12 @@ pub enum ContractError { #[error("Incentive creation fee was not included")] IncentiveFeeMissing, + #[error("Incentive end timestamp was set to a time in the past")] + IncentiveEndsInPast, + #[error("The incentive you are intending to create doesn't meet the minimum required of {min} after taking the fee")] EmptyIncentiveAfterFee { min: u128 }, - #[error("The asset sent doesn't match the asset expected")] - AssetMismatch, - #[error( "Incentive creation fee was not fulfilled, only {paid_amount} / {required_amount} present" )] @@ -89,12 +75,6 @@ pub enum ContractError { required_amount: Uint128, }, - #[error("The end epoch for this incentive is invalid")] - InvalidEndEpoch, - - #[error("Incentive end timestamp was set to a time in the past")] - IncentiveEndsInPast, - #[error("Incentive start timestamp is after the end timestamp")] IncentiveStartTimeAfterEndTime, @@ -107,12 +87,18 @@ pub enum ContractError { #[error("The incentive doesn't have enough funds to pay out the reward")] IncentiveExhausted, - #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] - MigrateInvalidVersion { - new_version: Version, - current_version: Version, + #[error("The asset sent doesn't match the asset expected")] + AssetMismatch, + + #[error("Attempt to create a new incentive, which exceeds the maximum of {max} incentives allowed per LP at a time")] + TooManyIncentives { + /// The maximum amount of incentives that can exist + max: u32, }, + #[error("The end epoch for this incentive is invalid")] + InvalidEndEpoch, + #[error("The sender doesn't have open positions")] NoOpenPositions, @@ -122,32 +108,27 @@ pub enum ContractError { #[error("The position has not expired yet")] PositionNotExpired, + #[error("The position with the identifier {identifier} is already closed")] + PositionAlreadyClosed { identifier: String }, + #[error( "Invalid unlocking duration of {specified} specified, must be between {min} and {max}" )] InvalidUnlockingDuration { - /// The minimum amount of seconds that a user must bond for. + /// The minimum amount of seconds that a user must lock for. min: u64, - /// The maximum amount of seconds that a user can bond for. + /// The maximum amount of seconds that a user can lock for. max: u64, - /// The amount of seconds the user attempted to bond for. + /// The amount of seconds the user attempted to lock for. specified: u64, }, - #[error("Attempt to create a position with {deposited_amount}, but only {allowance_amount} was set in allowance")] - MissingPositionDeposit { - /// The actual amount that the contract has an allowance for. - allowance_amount: Uint128, - /// The amount the account attempted to open a position with - deposited_amount: Uint128, - }, - - #[error("Attempt to create a position with {desired_amount}, but {paid_amount} was sent")] - MissingPositionDepositNative { - /// The amount the user intended to deposit. - desired_amount: Uint128, - /// The amount that was actually deposited. - paid_amount: Uint128, + #[error("Invalid unlocking range, specified min as {min} and max as {max}")] + InvalidUnlockingRange { + /// The minimum unlocking time + min: u64, + /// The maximum unlocking time + max: u64, }, #[error("Attempt to compute the weight of a duration of {unlocking_duration} which is outside the allowed bounds")] @@ -167,6 +148,12 @@ pub enum ContractError { #[error("There's no snapshot of the LP weight in the contract for the epoch {epoch_id}")] LpWeightNotFound { epoch_id: EpochId }, + + #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] + MigrateInvalidVersion { + new_version: Version, + current_version: Version, + }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index b53ecd5df..c8c0a6d63 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -118,26 +118,26 @@ pub(crate) fn validate_incentive_epochs( max_incentive_epoch_buffer: u64, ) -> Result<(u64, u64), ContractError> { // assert epoch params are correctly set + let start_epoch = params.start_epoch.unwrap_or(current_epoch); + let preliminary_end_epoch = params.preliminary_end_epoch.unwrap_or( - current_epoch + start_epoch .checked_add(DEFAULT_INCENTIVE_DURATION) .ok_or(ContractError::InvalidEndEpoch)?, ); - // ensure the incentive is set to end in a future epoch - ensure!( - preliminary_end_epoch > current_epoch, - ContractError::IncentiveEndsInPast - ); - - let start_epoch = params.start_epoch.unwrap_or(current_epoch); - // ensure that start date is before end date ensure!( start_epoch < preliminary_end_epoch, ContractError::IncentiveStartTimeAfterEndTime ); + // ensure the incentive is set to end in a future epoch + ensure!( + preliminary_end_epoch > current_epoch, + ContractError::IncentiveEndsInPast + ); + // ensure that start date is set within buffer ensure!( start_epoch diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index 00c14f02b..c43772acc 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -31,7 +31,7 @@ pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result Result return Err(ContractError::Unauthorized), } @@ -71,9 +79,6 @@ pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result Result Result { @@ -110,10 +115,11 @@ pub(crate) fn calculate_rewards( Some(config.max_concurrent_incentives), )?; - let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, &position.receiver)?; + let last_claimed_epoch_for_user = + LAST_CLAIMED_EPOCH.may_load(deps.storage, &position.receiver)?; // Check if the user ever claimed before - if let Some(last_claimed_epoch) = last_claimed_epoch { + if let Some(last_claimed_epoch) = last_claimed_epoch_for_user { // if the last claimed epoch is the same as the current epoch, then there is nothing to claim if current_epoch_id == last_claimed_epoch { return if is_claim { @@ -132,33 +138,32 @@ pub(crate) fn calculate_rewards( let mut modified_incentives: HashMap = HashMap::new(); for incentive in incentives { - println!("*********"); // skip expired incentives - if incentive.is_expired(current_epoch_id) { + if incentive.is_expired(current_epoch_id) || incentive.start_epoch > current_epoch_id { continue; } - println!("Incentive: {:?}", incentive); + // compute where the user can start claiming rewards for the incentive - let start_from_epoch = compute_start_from_epoch_for_incentive( + let start_from_epoch = compute_start_from_epoch_for_user( deps.storage, - &incentive, - last_claimed_epoch, + &incentive.lp_denom, + last_claimed_epoch_for_user, &position.receiver, )?; // compute the weights of the user for the epochs between start_from_epoch and current_epoch_id - let user_weights = compute_user_weights( - deps.storage, - &position.receiver, - &start_from_epoch, - ¤t_epoch_id, - )?; + let user_weights = + compute_user_weights(deps.storage, position, &start_from_epoch, ¤t_epoch_id)?; // compute the incentive emissions for the epochs between start_from_epoch and current_epoch_id let (incentive_emissions, until_epoch) = compute_incentive_emissions(&incentive, &start_from_epoch, ¤t_epoch_id)?; for epoch_id in start_from_epoch..=until_epoch { + if incentive.start_epoch > epoch_id { + continue; + } + let user_weight = user_weights[&epoch_id]; let total_lp_weight = LP_WEIGHTS_HISTORY .may_load(deps.storage, (&incentive.lp_denom, epoch_id))? @@ -184,11 +189,7 @@ pub(crate) fn calculate_rewards( amount: reward, }); } - println!("----"); - println!("epoch_id: {:?}", epoch_id); - println!("total_lp_weight: {:?}", total_lp_weight); - println!("user_share: {:?}", user_share); - println!("Reward: {:?}", reward); + if is_claim { // compound the rewards for the incentive let maybe_reward = modified_incentives @@ -217,9 +218,9 @@ pub(crate) fn calculate_rewards( } /// Computes the epoch from which the user can start claiming rewards for a given incentive -pub(crate) fn compute_start_from_epoch_for_incentive( +pub(crate) fn compute_start_from_epoch_for_user( storage: &dyn Storage, - incentive: &Incentive, + lp_denom: &str, last_claimed_epoch: Option, receiver: &Addr, ) -> Result { @@ -229,27 +230,22 @@ pub(crate) fn compute_start_from_epoch_for_incentive( } else { // if the user has never claimed before but has a weight, get the epoch at which the user // first had a weight in the system - get_earliest_address_lp_weight(storage, receiver)?.0 + get_earliest_address_lp_weight(storage, receiver, lp_denom)?.0 }; - // returns the latest epoch between the first claimable epoch for the user and the start epoch - // of the incentive, i.e. either when the incentive starts IF the incentive starts after the - // first claimable epoch for the user, or the first claimable epoch for the user IF the incentive - // started before the user had a weight in the system - Ok(incentive.start_epoch.max(first_claimable_epoch_for_user)) + Ok(first_claimable_epoch_for_user) } -/// Computes the user weights for a given LP asset. This assumes that [compute_start_from_epoch_for_incentive] +/// Computes the user weights for a given LP asset. This assumes that [compute_start_from_epoch_for_user] /// was called before this function, computing the start_from_epoch for the user with either the last_claimed_epoch /// or the first epoch the user had a weight in the system. pub(crate) fn compute_user_weights( storage: &dyn Storage, - receiver: &Addr, + position: &Position, start_from_epoch: &EpochId, current_epoch_id: &EpochId, ) -> Result, ContractError> { let mut user_weights = HashMap::new(); - let mut last_weight_seen = Uint128::zero(); // starts from start_from_epoch - 1 in case the user has a last_claimed_epoch, which means the user @@ -257,7 +253,11 @@ pub(crate) fn compute_user_weights( // last_claimed_epoch + 1 in that case, which is correct, and if the user has not modified its // position, the weight will be the same for start_from_epoch as it is for last_claimed_epoch. for epoch_id in *start_from_epoch - 1..=*current_epoch_id { - let weight = ADDRESS_LP_WEIGHT_HISTORY.may_load(storage, (receiver, epoch_id))?; + let weight = ADDRESS_LP_WEIGHT_HISTORY.may_load( + storage, + (&position.receiver, &position.lp_asset.denom, epoch_id), + )?; + if let Some(weight) = weight { last_weight_seen = weight; user_weights.insert(epoch_id, weight); @@ -265,7 +265,6 @@ pub(crate) fn compute_user_weights( user_weights.insert(epoch_id, last_weight_seen); } } - Ok(user_weights) } @@ -300,21 +299,22 @@ fn compute_incentive_emissions( fn sync_address_lp_weight_history( storage: &mut dyn Storage, address: &Addr, + lp_denom: &str, current_epoch_id: &u64, ) -> Result<(), ContractError> { - let (earliest_epoch_id, _) = get_earliest_address_lp_weight(storage, address)?; + let (earliest_epoch_id, _) = get_earliest_address_lp_weight(storage, address, lp_denom)?; let (latest_epoch_id, latest_address_lp_weight) = - get_latest_address_lp_weight(storage, address)?; + get_latest_address_lp_weight(storage, address, lp_denom)?; // remove previous entries for epoch_id in earliest_epoch_id..=latest_epoch_id { - ADDRESS_LP_WEIGHT_HISTORY.remove(storage, (address, epoch_id)); + ADDRESS_LP_WEIGHT_HISTORY.remove(storage, (address, lp_denom, epoch_id)); } // save the latest weight for the current epoch ADDRESS_LP_WEIGHT_HISTORY.save( storage, - (address, *current_epoch_id), + (address, lp_denom, *current_epoch_id), &latest_address_lp_weight, )?; diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs deleted file mode 100644 index 2e7a7f241..000000000 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/helpers.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::state::ADDRESS_LP_WEIGHT_HISTORY; -use cosmwasm_std::{Addr, StdResult, Storage, Uint128}; - -pub(crate) fn fill_address_lp_weight_history( - storage: &mut dyn Storage, - address: &Addr, - epoch_id: u64, - weight: Uint128, -) -> StdResult<()> { - ADDRESS_LP_WEIGHT_HISTORY.save(storage, (address, epoch_id), &weight) -} diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs index 699f16489..2850608c4 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/mod.rs @@ -1,2 +1 @@ -mod helpers; mod rewards; diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs index e3bbcf13f..6e6f417e6 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs @@ -1,12 +1,11 @@ -use crate::incentive::commands::{compute_start_from_epoch_for_incentive, compute_user_weights}; +use crate::incentive::commands::{compute_start_from_epoch_for_user, compute_user_weights}; use crate::state::ADDRESS_LP_WEIGHT_HISTORY; -use cosmwasm_std::{Addr, Coin, Storage, Uint128}; -use white_whale_std::incentive_manager::{Curve, EpochId, Incentive}; -use white_whale_std::pool_network::asset::{Asset, AssetInfo}; +use cosmwasm_std::{Addr, Coin, Uint128}; +use white_whale_std::incentive_manager::{Curve, Incentive, Position}; use white_whale_std::pool_network::mock_querier::mock_dependencies; #[test] -fn compute_start_from_epoch_for_incentive_successfully() { +fn compute_start_from_epoch_for_user_successfully() { let mut deps = mock_dependencies(&[]); let user = Addr::unchecked("user"); @@ -26,39 +25,37 @@ fn compute_start_from_epoch_for_incentive_successfully() { last_epoch_claimed: 9, }; - let current_epoch_id = 12u64; - // Mimics the scenario where the user has never claimed before, but opened a position before the incentive // went live let first_user_weight_epoch_id = 8; ADDRESS_LP_WEIGHT_HISTORY .save( &mut deps.storage, - (&user, first_user_weight_epoch_id), + (&user, "lp", first_user_weight_epoch_id), &Uint128::one(), ) .unwrap(); let start_from_epoch = - compute_start_from_epoch_for_incentive(&deps.storage, &incentive, None, &user).unwrap(); + compute_start_from_epoch_for_user(&deps.storage, &incentive.lp_denom, None, &user).unwrap(); // the function should return the start epoch of the incentive - assert_eq!(start_from_epoch, 10); + assert_eq!(start_from_epoch, first_user_weight_epoch_id); // Mimics the scenario where the user has never claimed before, but opened a position after the incentive // went live incentive.start_epoch = 5u64; let start_from_epoch = - compute_start_from_epoch_for_incentive(&deps.storage, &incentive, None, &user).unwrap(); + compute_start_from_epoch_for_user(&deps.storage, &incentive.lp_denom, None, &user).unwrap(); // the function should return the first epoch the user has a weight - assert_eq!(start_from_epoch, 8); + assert_eq!(start_from_epoch, first_user_weight_epoch_id); // Mimics the scenario where the user has claimed already, after the incentive went live, i.e. the user // has already partially claimed this incentive incentive.start_epoch = 10u64; let start_from_epoch = - compute_start_from_epoch_for_incentive(&deps.storage, &incentive, Some(12u64), &user) + compute_start_from_epoch_for_user(&deps.storage, &incentive.lp_denom, Some(12u64), &user) .unwrap(); // the function should return the next epoch after the last claimed one @@ -68,16 +65,16 @@ fn compute_start_from_epoch_for_incentive_successfully() { // has not claimed this incentive at all incentive.start_epoch = 15u64; let start_from_epoch = - compute_start_from_epoch_for_incentive(&deps.storage, &incentive, Some(12u64), &user) + compute_start_from_epoch_for_user(&deps.storage, &incentive.lp_denom, Some(12u64), &user) .unwrap(); // the function should return the start epoch of the incentive - assert_eq!(start_from_epoch, 15); + assert_eq!(start_from_epoch, 13); // Mimics the scenario where the user has claimed the epoch the incentives went live incentive.start_epoch = 15u64; let start_from_epoch = - compute_start_from_epoch_for_incentive(&deps.storage, &incentive, Some(15u64), &user) + compute_start_from_epoch_for_user(&deps.storage, &incentive.lp_denom, Some(15u64), &user) .unwrap(); // the function should return the next epoch after the last claimed one @@ -91,19 +88,36 @@ fn compute_user_weights_successfully() { let user = Addr::unchecked("user"); let mut start_from_epoch = 1u64; - let mut current_epoch_id = 10u64; + let current_epoch_id = 10u64; // fill the lp_weight_history for the address with // [(1,2), (2,4), (3,6), (4,8), (5,10), (6,12), (7,14), (8,16), (9,18), (10,20)] for epoch in 1u64..=10u64 { let weight = Uint128::new(epoch as u128 * 2u128); ADDRESS_LP_WEIGHT_HISTORY - .save(&mut deps.storage, (&user, epoch), &weight) + .save(&mut deps.storage, (&user, "lp", epoch), &weight) .unwrap(); } - let weights = - compute_user_weights(&deps.storage, &user, &start_from_epoch, ¤t_epoch_id).unwrap(); + let position = Position { + identifier: "1".to_string(), + lp_asset: Coin { + denom: "lp".to_string(), + amount: Default::default(), + }, + unlocking_duration: 86_400, + open: true, + expiring_at: None, + receiver: user.clone(), + }; + + let weights = compute_user_weights( + &deps.storage, + &position, + &start_from_epoch, + ¤t_epoch_id, + ) + .unwrap(); assert_eq!(weights.len(), 11); for epoch in 1u64..=10u64 { @@ -113,7 +127,8 @@ fn compute_user_weights_successfully() { ); // reset the weight for epochs - ADDRESS_LP_WEIGHT_HISTORY.remove(&mut deps.storage, (&user, epoch)); + ADDRESS_LP_WEIGHT_HISTORY + .remove(&mut deps.storage, (&user, &position.lp_asset.denom, epoch)); } // fill the lp_weight_history for the address with @@ -125,7 +140,11 @@ fn compute_user_weights_successfully() { let weight = Uint128::new(epoch as u128 * 2u128); ADDRESS_LP_WEIGHT_HISTORY - .save(&mut deps.storage, (&user, epoch), &weight) + .save( + &mut deps.storage, + (&user, &position.lp_asset.denom, epoch), + &weight, + ) .unwrap(); } @@ -133,8 +152,13 @@ fn compute_user_weights_successfully() { // value as the previous, most recent value, i.e. epoch 2 3 4 having the value of 1 (latest weight seen in epoch 1) // then 5..7 having the value of 10 (latest weight seen in epoch 5) // then 8..=10 having the value of 14 (latest weight seen in epoch 7) - let weights = - compute_user_weights(&deps.storage, &user, &start_from_epoch, ¤t_epoch_id).unwrap(); + let weights = compute_user_weights( + &deps.storage, + &position, + &start_from_epoch, + ¤t_epoch_id, + ) + .unwrap(); assert_eq!(weights.len(), 11); assert_eq!(weights.get(&1).unwrap(), &Uint128::new(2)); @@ -145,8 +169,13 @@ fn compute_user_weights_successfully() { assert_eq!(weights.get(&10).unwrap(), &Uint128::new(14)); start_from_epoch = 6u64; - let weights = - compute_user_weights(&deps.storage, &user, &start_from_epoch, ¤t_epoch_id).unwrap(); + let weights = compute_user_weights( + &deps.storage, + &position, + &start_from_epoch, + ¤t_epoch_id, + ) + .unwrap(); assert_eq!(weights.len(), 6); assert_eq!(weights.get(&5).unwrap(), &Uint128::new(10)); @@ -156,6 +185,7 @@ fn compute_user_weights_successfully() { for epoch in 1u64..=10u64 { // reset the weight for epochs - ADDRESS_LP_WEIGHT_HISTORY.remove(&mut deps.storage, (&user, epoch)); + ADDRESS_LP_WEIGHT_HISTORY + .remove(&mut deps.storage, (&user, &position.lp_asset.denom, epoch)); } } diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 0df26310a..955a611cf 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -385,7 +385,7 @@ pub(crate) fn update_config( if let Some(max_unlocking_duration) = max_unlocking_duration { if max_unlocking_duration < config.min_unlocking_duration { - return Err(ContractError::InvalidUnbondingRange { + return Err(ContractError::InvalidUnlockingRange { min: config.min_unlocking_duration, max: max_unlocking_duration, }); @@ -396,7 +396,7 @@ pub(crate) fn update_config( if let Some(min_unlocking_duration) = min_unlocking_duration { if config.max_unlocking_duration < min_unlocking_duration { - return Err(ContractError::InvalidUnbondingRange { + return Err(ContractError::InvalidUnlockingRange { min: min_unlocking_duration, max: config.max_unlocking_duration, }); diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 7ab90b8d9..3d86bde75 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -126,6 +126,11 @@ pub(crate) fn close_position( ContractError::Unauthorized ); + ensure!( + position.open, + ContractError::PositionAlreadyClosed { identifier } + ); + let mut attributes = vec![ ("action", "close_position".to_string()), ("receiver", info.sender.to_string()), @@ -191,7 +196,7 @@ pub(crate) fn close_position( /// Withdraws the given position. The position needs to have expired. pub(crate) fn withdraw_position( - deps: DepsMut, + mut deps: DepsMut, env: Env, info: MessageInfo, identifier: String, @@ -210,6 +215,15 @@ pub(crate) fn withdraw_position( ContractError::Unauthorized ); + // check if the user has pending rewards. Can't withdraw a position without claiming pending rewards first + let rewards_response = query_rewards(deps.as_ref(), info.sender.clone().into_string())?; + match rewards_response { + RewardsResponse::RewardsResponse { rewards } => { + ensure!(rewards.is_empty(), ContractError::PendingRewards) + } + _ => return Err(ContractError::Unauthorized), + } + let mut messages: Vec = vec![]; // check if the emergency unlock is requested, will pull the whole position out whether it's open, closed or expired, paying the penalty @@ -237,6 +251,18 @@ pub(crate) fn withdraw_position( vec![penalty], )?); + // if the position is open, update the weights when doing the emergency withdrawal + // otherwise not, as the weights have already being updated when the position was closed + if position.open { + update_weights( + deps.branch(), + &info, + &position.lp_asset, + position.unlocking_duration, + false, + )?; + } + // subtract the penalty from the original position position.lp_asset.amount = position.lp_asset.amount.saturating_sub(penalty_fee); } else { @@ -306,7 +332,8 @@ fn update_weights( )?; // update the user's weight for this LP - let (_, mut address_lp_weight) = get_latest_address_weight(deps.storage, &receiver.sender)?; + let (_, mut address_lp_weight) = + get_latest_address_weight(deps.storage, &receiver.sender, &lp_asset.denom)?; if fill { // filling position @@ -319,7 +346,7 @@ fn update_weights( //todo if the address weight is zero, remove it from the storage? ADDRESS_LP_WEIGHT_HISTORY.update::<_, StdError>( deps.storage, - (&receiver.sender, current_epoch.id + 1u64), + (&receiver.sender, &lp_asset.denom, current_epoch.id + 1u64), |_| Ok(address_lp_weight), )?; diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index c10748dc2..fdc1cdc09 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -60,9 +60,10 @@ pub fn calculate_weight( pub fn get_latest_address_weight( storage: &dyn Storage, address: &Addr, + lp_denom: &str, ) -> Result<(EpochId, Uint128), ContractError> { let result = ADDRESS_LP_WEIGHT_HISTORY - .prefix(address) + .prefix((address, lp_denom)) .range(storage, None, None, Order::Descending) .take(1usize) // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. diff --git a/contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs b/contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs index 53861a476..3cff1fe33 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/tests/weight_calculation.rs @@ -2,31 +2,24 @@ fn test_calculate_weight() { use crate::position::helpers::calculate_weight; use cosmwasm_std::coin; + use cosmwasm_std::Uint128; let weight = calculate_weight(&coin(100, "uwhale"), 86400u64).unwrap(); - println!("1 day Weight: {:?}", weight); - - let weight = calculate_weight(&coin(100, "uwhale"), 1209600).unwrap(); - println!("2 weeks Weight: {:?}", weight); + assert_eq!(weight, Uint128::new(100)); + // 1 month let weight = calculate_weight(&coin(100, "uwhale"), 2629746).unwrap(); - println!("1 month Weight: {:?}", weight); - - let weight = calculate_weight(&coin(100, "uwhale"), 5259492).unwrap(); - println!("2 months Weight: {:?}", weight); + assert_eq!(weight, Uint128::new(117)); + // 3 months let weight = calculate_weight(&coin(100, "uwhale"), 7889238).unwrap(); - println!("3 months Weight: {:?}", weight); - - let weight = calculate_weight(&coin(100, "uwhale"), 10518984).unwrap(); - println!("4 months Weight: {:?}", weight); - - let weight = calculate_weight(&coin(100, "uwhale"), 13148730).unwrap(); - println!("5 months Weight: {:?}", weight); + assert_eq!(weight, Uint128::new(212)); + // 6 months let weight = calculate_weight(&coin(100, "uwhale"), 15778476).unwrap(); - println!("6 months Weight: {:?}", weight); + assert_eq!(weight, Uint128::new(500)); + // 1 year let weight = calculate_weight(&coin(100, "uwhale"), 31556926).unwrap(); - println!("1 year Weight: {:?}", weight); + assert_eq!(weight, Uint128::new(1599)); } diff --git a/contracts/liquidity_hub/incentive-manager/src/queries.rs b/contracts/liquidity_hub/incentive-manager/src/queries.rs index e23e59efd..ffd613a66 100644 --- a/contracts/liquidity_hub/incentive-manager/src/queries.rs +++ b/contracts/liquidity_hub/incentive-manager/src/queries.rs @@ -80,7 +80,7 @@ pub(crate) fn query_rewards(deps: Deps, address: String) -> Result = Map::new("lp_weigh //todo add the lp denom here as well, otherwise there's no way to distinguish /// The address lp weight history, i.e. how much lp weight an address had at a given epoch -pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, EpochId), Uint128> = +pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, &str, EpochId), Uint128> = Map::new("address_lp_weight_history"); /// An monotonically increasing counter to generate unique incentive identifiers. @@ -212,9 +212,10 @@ pub fn get_positions_by_receiver( pub fn get_earliest_address_lp_weight( storage: &dyn Storage, address: &Addr, + lp_denom: &str, ) -> Result<(EpochId, Uint128), ContractError> { let earliest_weight_history_result = ADDRESS_LP_WEIGHT_HISTORY - .prefix(address) + .prefix((address, lp_denom)) .range(storage, None, None, Order::Ascending) .next() .transpose(); @@ -231,9 +232,10 @@ pub fn get_earliest_address_lp_weight( pub fn get_latest_address_lp_weight( storage: &dyn Storage, address: &Addr, + lp_denom: &str, ) -> Result<(EpochId, Uint128), ContractError> { let latest_weight_history_result = ADDRESS_LP_WEIGHT_HISTORY - .prefix(address) + .prefix((address, lp_denom)) .range(storage, None, None, Order::Descending) .next() .transpose(); diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index 7a323160b..b17b892be 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -12,15 +12,12 @@ use white_whale_std::incentive_manager::{ Config, IncentiveAction, IncentivesBy, IncentivesResponse, InstantiateMsg, LpWeightResponse, PositionAction, PositionsResponse, RewardsResponse, }; -use white_whale_std::pool_network::asset::{Asset, AssetInfo, PairType}; -use white_whale_std::pool_network::pair::ExecuteMsg::ProvideLiquidity; -use white_whale_std::pool_network::pair::{PoolFee, SimulationResponse}; +use white_whale_std::pool_network::asset::AssetInfo; use white_whale_testing::multi_test::stargate_mock::StargateMock; use crate::common::suite_contracts::{ epoch_manager_contract, incentive_manager_contract, whale_lair_contract, }; -use crate::common::MOCK_CONTRACT_ADDR; type OsmosisTokenFactoryApp = App< BankKeeper, @@ -64,13 +61,6 @@ impl TestingSuite { self } - pub(crate) fn add_half_a_day(&mut self) -> &mut Self { - let mut block_info = self.app.block_info(); - block_info.time = block_info.time.plus_hours(12); - self.app.set_block(block_info); - - self - } pub(crate) fn add_one_epoch(&mut self) -> &mut Self { let creator = self.creator(); diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index d53882174..65980c556 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -56,7 +56,7 @@ fn instantiate_incentive_manager() { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::InvalidUnbondingRange { .. } => {} + ContractError::InvalidUnlockingRange { .. } => {} _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), } }, @@ -278,11 +278,12 @@ fn create_incentives() { }, vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], |result| { + let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::IncentiveStartTimeAfterEndTime { .. } => {} - _ => panic!("Wrong error type, should return ContractError::IncentiveStartTimeAfterEndTime"), + ContractError::IncentiveStartTooFar { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveStartTooFar"), } }, ) @@ -306,8 +307,8 @@ fn create_incentives() { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::IncentiveEndsInPast { .. } => {} - _ => panic!("Wrong error type, should return ContractError::IncentiveEndsInPast"), + ContractError::IncentiveStartTimeAfterEndTime { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveStartTimeAfterEndTime"), } }, ).manage_incentive( @@ -335,6 +336,32 @@ fn create_incentives() { } }, ).manage_incentive( + other.clone(), + IncentiveAction::Fill { + params: IncentiveParams { + lp_denom: lp_denom.clone(), + start_epoch: Some(3), + preliminary_end_epoch: Some(5), + curve: None, + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(4_000u128), + }, + incentive_identifier: None, + }, + }, + vec![coin(4_000, "ulab"), coin(1_000, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::IncentiveEndsInPast { .. } => {} + _ => panic!("Wrong error type, should return ContractError::IncentiveEndsInPast"), + } + }, + ) + + .manage_incentive( other.clone(), IncentiveAction::Fill { params: IncentiveParams { @@ -898,7 +925,6 @@ pub fn update_config() { let creator = suite.creator(); let other = suite.senders[1].clone(); - let another = suite.senders[2].clone(); suite.instantiate_default(); @@ -1003,7 +1029,7 @@ pub fn update_config() { |result| { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::InvalidUnbondingRange { .. } => {} + ContractError::InvalidUnlockingRange { .. } => {} _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), } }, @@ -1024,7 +1050,7 @@ pub fn update_config() { |result| { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::InvalidUnbondingRange { .. } => {} + ContractError::InvalidUnlockingRange { .. } => {} _ => panic!("Wrong error type, should return ContractError::InvalidUnbondingRange"), } }, @@ -1107,6 +1133,7 @@ pub fn test_manage_position() { suite.instantiate_default(); let incentive_manager = suite.incentive_manager_addr.clone(); + let whale_lair = suite.whale_lair_addr.clone(); suite .add_hook(creator.clone(), incentive_manager, vec![], |result| { @@ -1699,6 +1726,21 @@ pub fn test_manage_position() { Uint128::new(2_000) ); }) + .manage_position( + creator.clone(), + PositionAction::Withdraw { + identifier: "2".to_string(), + emergency_unlock: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PendingRewards { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PendingRewards"), + } + }, + ) .claim(creator.clone(), vec![], |result| { result.unwrap(); }) @@ -1823,6 +1865,124 @@ pub fn test_manage_position() { } ); }); + + suite + .add_one_epoch() + .add_one_epoch() + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 18); + }); + + // try emergency exit a position that is closed + suite + .manage_position( + another.clone(), + PositionAction::Fill { + identifier: Some("special_position".to_string()), + unlocking_duration: 100_000, + receiver: None, + }, + vec![coin(5_000, lp_denom.clone())], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 18, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + lp_weight: Uint128::new(5_000), + epoch_id: 18, + } + ); + }) + .query_lp_weight(&lp_denom, 19, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + lp_weight: Uint128::new(10_002), + epoch_id: 19, + } + ); + }); + + suite.add_one_epoch().query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 19); + }); + + // close the position + suite + .manage_position( + another.clone(), + PositionAction::Close { + identifier: "special_position".to_string(), + lp_asset: None, + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_lp_weight(&lp_denom, 20, |result| { + let lp_weight = result.unwrap(); + assert_eq!( + lp_weight, + LpWeightResponse { + // the weight went back to what it was before the position was opened + lp_weight: Uint128::new(5_000), + epoch_id: 20, + } + ); + }); + + // emergency exit + suite + .query_balance( + lp_denom.clone().to_string(), + whale_lair.clone(), + |balance| { + assert_eq!(balance, Uint128::zero()); + }, + ) + .manage_position( + another.clone(), + PositionAction::Close { + identifier: "special_position".to_string(), + lp_asset: None, + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PositionAlreadyClosed { .. } => {} + _ => panic!( + "Wrong error type, should return ContractError::PositionAlreadyClosed" + ), + } + }, + ) + .manage_position( + another.clone(), + PositionAction::Withdraw { + identifier: "special_position".to_string(), + emergency_unlock: Some(true), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_balance( + lp_denom.clone().to_string(), + whale_lair.clone(), + |balance| { + assert_eq!(balance, Uint128::new(500)); + }, + ); } #[test] @@ -2178,7 +2338,6 @@ fn test_emergency_withdrawal() { coin(1_000_000_000u128, lp_denom.clone()), ]); - let creator = suite.creator(); let other = suite.senders[1].clone(); suite.instantiate_default(); @@ -2468,7 +2627,7 @@ fn test_multiple_incentives_and_positions() { params: IncentiveParams { lp_denom: lp_denom_1.clone(), start_epoch: Some(14), - preliminary_end_epoch: Some(25), + preliminary_end_epoch: Some(24), curve: None, incentive_asset: Coin { denom: "uosmo".to_string(), @@ -2528,7 +2687,7 @@ fn test_multiple_incentives_and_positions() { .manage_position( creator.clone(), PositionAction::Fill { - identifier: None, + identifier: Some("creator_pos_1".to_string()), unlocking_duration: 86_400, receiver: None, }, @@ -2540,11 +2699,11 @@ fn test_multiple_incentives_and_positions() { .manage_position( creator.clone(), PositionAction::Fill { - identifier: None, + identifier: Some("creator_pos_2".to_string()), unlocking_duration: 86_400, receiver: None, }, - vec![coin(35_000, lp_denom_2.clone())], + vec![coin(70_000, lp_denom_2.clone())], |result| { result.unwrap(); }, @@ -2564,7 +2723,7 @@ fn test_multiple_incentives_and_positions() { .manage_position( other.clone(), PositionAction::Fill { - identifier: None, + identifier: Some("other_pos_1".to_string()), unlocking_duration: 86_400, receiver: None, }, @@ -2576,11 +2735,11 @@ fn test_multiple_incentives_and_positions() { .manage_position( other.clone(), PositionAction::Fill { - identifier: None, + identifier: Some("other_pos_2".to_string()), unlocking_duration: 86_400, receiver: None, }, - vec![coin(40_000, lp_denom_2.clone())], + vec![coin(80_000, lp_denom_2.clone())], |result| { result.unwrap(); }, @@ -2621,33 +2780,460 @@ fn test_multiple_incentives_and_positions() { ); }, ) + .query_balance("ulab".to_string(), creator.clone(), |balance| { + assert_eq!(balance, Uint128::new(999_920_000)); + }) .claim(creator.clone(), vec![], |result| { result.unwrap(); }) - .query_incentives( - Some(IncentivesBy::Identifier("incentive_1".to_string())), - None, - None, + .query_balance("ulab".to_string(), creator.clone(), |balance| { + assert_eq!(balance, Uint128::new(999_978_666)); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0], + Incentive { + identifier: "incentive_1".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(80_000u128), + }, + claimed_amount: Uint128::new(58_666), + emission_rate: Uint128::new(20_000), + curve: Curve::Linear, + start_epoch: 12u64, + preliminary_end_epoch: 16u64, + last_epoch_claimed: 15u64, + } + ); + assert_eq!( + incentives_response.incentives[1], + Incentive { + identifier: "incentive_2".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(10_000u128), + }, + claimed_amount: Uint128::new(932), + emission_rate: Uint128::new(1_000), + curve: Curve::Linear, + start_epoch: 14u64, + preliminary_end_epoch: 24u64, + last_epoch_claimed: 15u64, + } + ); + }); + + suite + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 19); + }); + + // other emergency unlocks mid-way incentive 2 + suite + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(999_930_000)); + }) + .query_balance("uosmo".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(1_000_000_000)); + }) + .claim(other.clone(), vec![], |result| { + result.unwrap(); + }) + .query_balance("ulab".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(999_951_332)); + }) + .query_balance("uosmo".to_string(), other.clone(), |balance| { + assert_eq!(balance, Uint128::new(1_000_003_198)); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0], + Incentive { + identifier: "incentive_1".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(80_000u128), + }, + claimed_amount: Uint128::new(79_998u128), // exhausted + emission_rate: Uint128::new(20_000), + curve: Curve::Linear, + start_epoch: 12u64, + preliminary_end_epoch: 16u64, + last_epoch_claimed: 19u64, + } + ); + assert_eq!( + incentives_response.incentives[1], + Incentive { + identifier: "incentive_2".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(10_000u128), + }, + claimed_amount: Uint128::new(4_130), + emission_rate: Uint128::new(1_000), + curve: Curve::Linear, + start_epoch: 14u64, + preliminary_end_epoch: 24u64, + last_epoch_claimed: 19u64, + } + ); + }) + .manage_position( + other.clone(), + PositionAction::Withdraw { + identifier: "other_pos_1".to_string(), + emergency_unlock: Some(true), + }, + vec![], |result| { - let incentives_response = result.unwrap(); - assert_eq!( - incentives_response.incentives[0], - Incentive { - identifier: "incentive_1".to_string(), - owner: creator.clone(), - lp_denom: lp_denom_1.clone(), - incentive_asset: Coin { - denom: "ulab".to_string(), - amount: Uint128::new(80_000u128), - }, - claimed_amount: Uint128::new(58_666), - emission_rate: Uint128::new(20_000), - curve: Curve::Linear, - start_epoch: 12u64, - preliminary_end_epoch: 16u64, - last_epoch_claimed: 15u64, - } - ); + result.unwrap(); + }, + ) + .manage_position( + other.clone(), + PositionAction::Withdraw { + identifier: "other_pos_2".to_string(), + emergency_unlock: Some(true), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_balance( + lp_denom_1.clone().to_string(), + whale_lair_addr.clone(), + |balance| { + // 10% of the lp the user input initially + assert_eq!(balance, Uint128::new(4_000)); + }, + ) + .query_balance( + lp_denom_2.clone().to_string(), + whale_lair_addr.clone(), + |balance| { + // 10% of the lp the user input initially + assert_eq!(balance, Uint128::new(8_000)); }, ); + + // at this point, other doesn't have any positions, and creator owns 100% of the weight + + suite.add_one_epoch().query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 20); + }); + + // another fills a position + suite.manage_position( + another.clone(), + PositionAction::Fill { + identifier: Some("another_pos_1".to_string()), + unlocking_duration: 15_778_476, // 6 months, should give him 5x multiplier + receiver: None, + }, + vec![coin(6_000, lp_denom_2.clone())], + |result| { + result.unwrap(); + }, + ); + + // creator that had 100% now has ~70% of the weight, while another has ~30% + suite + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 30); + }); + + suite + .claim(creator.clone(), vec![], |result| { + // creator claims from epoch 16 to 30 + // There's nothing to claim on incentive 1 + // On incentive 2, creator has a portion of the total weight until the epoch where other + // triggered the emergency withdrawal. From that point (epoch 20) it has 100% of the weight + // for lp_denom_1. + // another never locked for lp_denom_1, so creator gets all the rewards for the incentive 2 + // from epoch 20 till it finishes at epoch 23 + result.unwrap(); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0], + Incentive { + identifier: "incentive_1".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(80_000u128), + }, + claimed_amount: Uint128::new(79_998u128), // exhausted + emission_rate: Uint128::new(20_000), + curve: Curve::Linear, + start_epoch: 12u64, + preliminary_end_epoch: 16u64, + last_epoch_claimed: 19u64, + } + ); + assert_eq!( + incentives_response.incentives[1], + Incentive { + identifier: "incentive_2".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(10_000u128), + }, + claimed_amount: Uint128::new(9_994), // exhausted + emission_rate: Uint128::new(1_000), + curve: Curve::Linear, + start_epoch: 14u64, + preliminary_end_epoch: 24u64, + last_epoch_claimed: 30u64, + } + ); + assert_eq!( + incentives_response.incentives[2], + Incentive { + identifier: "incentive_3".to_string(), + owner: other.clone(), + lp_denom: lp_denom_2.clone(), + incentive_asset: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(30_000u128), + }, + claimed_amount: Uint128::new(24_000), + emission_rate: Uint128::new(10_000), + curve: Curve::Linear, + start_epoch: 20u64, + preliminary_end_epoch: 23u64, + last_epoch_claimed: 30u64, + } + ); + assert_eq!( + incentives_response.incentives[3], + Incentive { + identifier: "incentive_4".to_string(), + owner: other.clone(), + lp_denom: lp_denom_2.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(70_000u128), + }, + claimed_amount: Uint128::new(28_000), + emission_rate: Uint128::new(5_000), + curve: Curve::Linear, + start_epoch: 23u64, + preliminary_end_epoch: 37u64, + last_epoch_claimed: 30u64, + } + ); + }) + .claim(another.clone(), vec![], |result| { + result.unwrap(); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[0], + Incentive { + identifier: "incentive_1".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(80_000u128), + }, + claimed_amount: Uint128::new(79_998u128), // exhausted + emission_rate: Uint128::new(20_000), + curve: Curve::Linear, + start_epoch: 12u64, + preliminary_end_epoch: 16u64, + last_epoch_claimed: 19u64, + } + ); + assert_eq!( + incentives_response.incentives[1], + Incentive { + identifier: "incentive_2".to_string(), + owner: creator.clone(), + lp_denom: lp_denom_1.clone(), + incentive_asset: Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(10_000u128), + }, + claimed_amount: Uint128::new(9_994), // exhausted + emission_rate: Uint128::new(1_000), + curve: Curve::Linear, + start_epoch: 14u64, + preliminary_end_epoch: 24u64, + last_epoch_claimed: 30u64, + } + ); + assert_eq!( + incentives_response.incentives[2], + Incentive { + identifier: "incentive_3".to_string(), + owner: other.clone(), + lp_denom: lp_denom_2.clone(), + incentive_asset: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(30_000u128), + }, + claimed_amount: Uint128::new(30_000), // exhausted + emission_rate: Uint128::new(10_000), + curve: Curve::Linear, + start_epoch: 20u64, + preliminary_end_epoch: 23u64, + last_epoch_claimed: 30u64, + } + ); + assert_eq!( + incentives_response.incentives[3], + Incentive { + identifier: "incentive_4".to_string(), + owner: other.clone(), + lp_denom: lp_denom_2.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(70_000u128), + }, + claimed_amount: Uint128::new(40_000), + emission_rate: Uint128::new(5_000), + curve: Curve::Linear, + start_epoch: 23u64, + preliminary_end_epoch: 37u64, + last_epoch_claimed: 30u64, + } + ); + }); + + // another closes part of his position mid-way through incentive 4. + // since the total weight was 100k and he unlocked 50% of his position, + // the new total weight is 85k, so he gets 15k/85k of the rewards while creator gets the rest + suite.manage_position( + another.clone(), + PositionAction::Close { + identifier: "another_pos_1".to_string(), + lp_asset: Some(coin(3_000, lp_denom_2.clone())), + }, + vec![], + |result| { + result.unwrap(); + }, + ); + + suite + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 35); + }); + + suite + .claim(creator.clone(), vec![], |result| { + result.unwrap(); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[3], + Incentive { + identifier: "incentive_4".to_string(), + owner: other.clone(), + lp_denom: lp_denom_2.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(70_000u128), + }, + claimed_amount: Uint128::new(60_585), + emission_rate: Uint128::new(5_000), + curve: Curve::Linear, + start_epoch: 23u64, + preliminary_end_epoch: 37u64, + last_epoch_claimed: 35u64, + } + ); + }) + .claim(another.clone(), vec![], |result| { + result.unwrap(); + }) + .query_incentives(None, None, None, |result| { + let incentives_response = result.unwrap(); + assert_eq!( + incentives_response.incentives[3], + Incentive { + identifier: "incentive_4".to_string(), + owner: other.clone(), + lp_denom: lp_denom_2.clone(), + incentive_asset: Coin { + denom: "ulab".to_string(), + amount: Uint128::new(70_000u128), + }, + claimed_amount: Uint128::new(64_995), + emission_rate: Uint128::new(5_000), + curve: Curve::Linear, + start_epoch: 23u64, + preliminary_end_epoch: 37u64, + last_epoch_claimed: 35u64, + } + ); + }); + + // now the epochs go by, the incentive expires and the creator withdraws the rest of the incentive + + suite + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .add_one_epoch() + .query_current_epoch(|result| { + let epoch_response = result.unwrap(); + assert_eq!(epoch_response.epoch.id, 40); + }); + + suite.manage_incentive( + creator.clone(), + IncentiveAction::Close { + incentive_identifier: "incentive_4".to_string(), + }, + vec![], + |result| { + result.unwrap(); + }, + ); } From 98af03709f48a450ac340e4c8517ab9d0cfa0550 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 11 Apr 2024 12:02:36 +0100 Subject: [PATCH 31/35] refactor: improve state by reusing map for contract lp weights --- .../incentive-manager/src/contract.rs | 19 +++++---- .../src/incentive/commands.rs | 27 +++++++----- .../src/incentive/tests/rewards.rs | 14 +++---- .../incentive-manager/src/manager/commands.rs | 24 +++++++---- .../src/position/commands.rs | 31 ++++++++------ .../incentive-manager/src/position/helpers.rs | 20 +-------- .../incentive-manager/src/queries.rs | 20 ++++++--- .../incentive-manager/src/state.rs | 41 ++++--------------- .../incentive-manager/tests/common/suite.rs | 1 + .../white-whale-std/src/incentive_manager.rs | 2 + 10 files changed, 96 insertions(+), 103 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index c806df944..f50e8a643 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -108,7 +108,7 @@ pub fn execute( ExecuteMsg::EpochChangedHook(msg) => { manager::commands::on_epoch_changed(deps, env, info, msg) } - ExecuteMsg::Claim => incentive::commands::claim(deps, info), + ExecuteMsg::Claim => incentive::commands::claim(deps, env, info), ExecuteMsg::ManagePosition { action } => match action { PositionAction::Fill { identifier, @@ -116,6 +116,7 @@ pub fn execute( receiver, } => position::commands::fill_position( deps, + &env, info, identifier, unlocking_duration, @@ -160,7 +161,7 @@ pub fn execute( } #[entry_point] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { QueryMsg::Config => Ok(to_json_binary(&queries::query_manager_config(deps)?)?), QueryMsg::Ownership {} => Ok(to_json_binary(&cw_ownable::get_ownership(deps.storage)?)?), @@ -180,11 +181,15 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Ok(to_json_binary(&queries::query_positions( deps, address, open_state, )?)?), - QueryMsg::Rewards { address } => { - Ok(to_json_binary(&queries::query_rewards(deps, address)?)?) - } - QueryMsg::LPWeight { denom, epoch_id } => Ok(to_json_binary(&queries::query_lp_weight( - deps, denom, epoch_id, + QueryMsg::Rewards { address } => Ok(to_json_binary(&queries::query_rewards( + deps, &env, address, + )?)?), + QueryMsg::LPWeight { + address, + denom, + epoch_id, + } => Ok(to_json_binary(&queries::query_lp_weight( + deps, address, denom, epoch_id, )?)?), } } diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs index c43772acc..8e0cfd85b 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/commands.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use cosmwasm_std::{ - ensure, Addr, BankMsg, Coin, CosmosMsg, Deps, DepsMut, MessageInfo, Response, Storage, Uint128, + ensure, Addr, BankMsg, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, Storage, + Uint128, }; use white_whale_std::coin::aggregate_coins; @@ -9,13 +10,12 @@ use white_whale_std::incentive_manager::{EpochId, Incentive, Position, RewardsRe use crate::state::{ get_earliest_address_lp_weight, get_incentives_by_lp_denom, get_latest_address_lp_weight, - get_positions_by_receiver, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, INCENTIVES, LAST_CLAIMED_EPOCH, - LP_WEIGHTS_HISTORY, + get_positions_by_receiver, CONFIG, INCENTIVES, LAST_CLAIMED_EPOCH, LP_WEIGHT_HISTORY, }; use crate::ContractError; /// Claims pending rewards for incentives where the user has LP -pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result { +pub(crate) fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { cw_utils::nonpayable(&info)?; // check if the user has any open LP positions @@ -33,7 +33,8 @@ pub(crate) fn claim(deps: DepsMut, info: MessageInfo) -> Result Result Result<(), ContractError> { let (earliest_epoch_id, _) = get_earliest_address_lp_weight(storage, address, lp_denom)?; let (latest_epoch_id, latest_address_lp_weight) = - get_latest_address_lp_weight(storage, address, lp_denom)?; + get_latest_address_lp_weight(storage, address, lp_denom, current_epoch_id)?; // remove previous entries for epoch_id in earliest_epoch_id..=latest_epoch_id { - ADDRESS_LP_WEIGHT_HISTORY.remove(storage, (address, lp_denom, epoch_id)); + LP_WEIGHT_HISTORY.remove(storage, (address, lp_denom, epoch_id)); } // save the latest weight for the current epoch - ADDRESS_LP_WEIGHT_HISTORY.save( + LP_WEIGHT_HISTORY.save( storage, (address, lp_denom, *current_epoch_id), &latest_address_lp_weight, diff --git a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs index 6e6f417e6..4913ba6dc 100644 --- a/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs +++ b/contracts/liquidity_hub/incentive-manager/src/incentive/tests/rewards.rs @@ -1,5 +1,5 @@ use crate::incentive::commands::{compute_start_from_epoch_for_user, compute_user_weights}; -use crate::state::ADDRESS_LP_WEIGHT_HISTORY; +use crate::state::LP_WEIGHT_HISTORY; use cosmwasm_std::{Addr, Coin, Uint128}; use white_whale_std::incentive_manager::{Curve, Incentive, Position}; use white_whale_std::pool_network::mock_querier::mock_dependencies; @@ -28,7 +28,7 @@ fn compute_start_from_epoch_for_user_successfully() { // Mimics the scenario where the user has never claimed before, but opened a position before the incentive // went live let first_user_weight_epoch_id = 8; - ADDRESS_LP_WEIGHT_HISTORY + LP_WEIGHT_HISTORY .save( &mut deps.storage, (&user, "lp", first_user_weight_epoch_id), @@ -94,7 +94,7 @@ fn compute_user_weights_successfully() { // [(1,2), (2,4), (3,6), (4,8), (5,10), (6,12), (7,14), (8,16), (9,18), (10,20)] for epoch in 1u64..=10u64 { let weight = Uint128::new(epoch as u128 * 2u128); - ADDRESS_LP_WEIGHT_HISTORY + LP_WEIGHT_HISTORY .save(&mut deps.storage, (&user, "lp", epoch), &weight) .unwrap(); } @@ -127,8 +127,7 @@ fn compute_user_weights_successfully() { ); // reset the weight for epochs - ADDRESS_LP_WEIGHT_HISTORY - .remove(&mut deps.storage, (&user, &position.lp_asset.denom, epoch)); + LP_WEIGHT_HISTORY.remove(&mut deps.storage, (&user, &position.lp_asset.denom, epoch)); } // fill the lp_weight_history for the address with @@ -139,7 +138,7 @@ fn compute_user_weights_successfully() { } let weight = Uint128::new(epoch as u128 * 2u128); - ADDRESS_LP_WEIGHT_HISTORY + LP_WEIGHT_HISTORY .save( &mut deps.storage, (&user, &position.lp_asset.denom, epoch), @@ -185,7 +184,6 @@ fn compute_user_weights_successfully() { for epoch in 1u64..=10u64 { // reset the weight for epochs - ADDRESS_LP_WEIGHT_HISTORY - .remove(&mut deps.storage, (&user, &position.lp_asset.denom, epoch)); + LP_WEIGHT_HISTORY.remove(&mut deps.storage, (&user, &position.lp_asset.denom, epoch)); } } diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 955a611cf..8aef989c6 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -13,8 +13,8 @@ use crate::helpers::{ validate_incentive_epochs, }; use crate::state::{ - get_incentive_by_identifier, get_incentives_by_lp_denom, get_latest_lp_weight_record, CONFIG, - INCENTIVES, INCENTIVE_COUNTER, LP_WEIGHTS_HISTORY, + get_incentive_by_identifier, get_incentives_by_lp_denom, get_latest_address_lp_weight, CONFIG, + INCENTIVES, INCENTIVE_COUNTER, LP_WEIGHT_HISTORY, }; use crate::ContractError; @@ -300,7 +300,7 @@ pub(crate) fn on_epoch_changed( // get all LP tokens and update the LP_WEIGHTS_HISTORY let lp_denoms = deps .querier - .query_all_balances(env.contract.address)? + .query_all_balances(env.contract.address.clone())? .into_iter() .filter(|asset| { if is_factory_token(asset.denom.as_str()) { @@ -314,8 +314,10 @@ pub(crate) fn on_epoch_changed( .collect::>(); for lp_denom in &lp_denoms { - let lp_weight_option = - LP_WEIGHTS_HISTORY.may_load(deps.storage, (lp_denom, msg.current_epoch.id))?; + let lp_weight_option = LP_WEIGHT_HISTORY.may_load( + deps.storage, + (&env.contract.address, lp_denom, msg.current_epoch.id), + )?; // if the weight for this LP token at this epoch has already been recorded, i.e. someone // opened or closed positions in the previous epoch, skip it @@ -324,12 +326,16 @@ pub(crate) fn on_epoch_changed( } else { // if the weight for this LP token at this epoch has not been recorded, i.e. no one // opened or closed positions in the previous epoch, get the last recorded weight - let (_, latest_lp_weight_record) = - get_latest_lp_weight_record(deps.storage, lp_denom, msg.current_epoch.id)?; + let (_, latest_lp_weight_record) = get_latest_address_lp_weight( + deps.storage, + &env.contract.address, + lp_denom, + &msg.current_epoch.id, + )?; - LP_WEIGHTS_HISTORY.save( + LP_WEIGHT_HISTORY.save( deps.storage, - (lp_denom, msg.current_epoch.id), + (&env.contract.address, lp_denom, msg.current_epoch.id), &latest_lp_weight_record, )?; } diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 3d86bde75..0b382d345 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -5,17 +5,15 @@ use cosmwasm_std::{ use white_whale_std::incentive_manager::{Position, RewardsResponse}; use crate::position::helpers::validate_unlocking_duration; -use crate::position::helpers::{calculate_weight, get_latest_address_weight, get_latest_lp_weight}; +use crate::position::helpers::{calculate_weight, get_latest_address_weight}; use crate::queries::query_rewards; -use crate::state::{ - get_position, ADDRESS_LP_WEIGHT_HISTORY, CONFIG, LP_WEIGHTS_HISTORY, POSITIONS, - POSITION_ID_COUNTER, -}; +use crate::state::{get_position, CONFIG, LP_WEIGHT_HISTORY, POSITIONS, POSITION_ID_COUNTER}; use crate::ContractError; /// Fills a position. If the position already exists, it will be expanded. Otherwise, a new position is created. pub(crate) fn fill_position( deps: DepsMut, + env: &Env, info: MessageInfo, identifier: Option, unlocking_duration: u64, @@ -81,7 +79,7 @@ pub(crate) fn fill_position( } // Update weights for the LP and the user - update_weights(deps, &receiver, &lp_asset, unlocking_duration, true)?; + update_weights(deps, env, &receiver, &lp_asset, unlocking_duration, true)?; let action = match position { Some(_) => "expand_position", @@ -107,7 +105,7 @@ pub(crate) fn close_position( cw_utils::nonpayable(&info)?; // check if the user has pending rewards. Can't close a position without claiming pending rewards first - let rewards_response = query_rewards(deps.as_ref(), info.sender.clone().into_string())?; + let rewards_response = query_rewards(deps.as_ref(), &env, info.sender.clone().into_string())?; match rewards_response { RewardsResponse::RewardsResponse { rewards } => { ensure!(rewards.is_empty(), ContractError::PendingRewards) @@ -183,6 +181,7 @@ pub(crate) fn close_position( update_weights( deps.branch(), + &env, &info, &position.lp_asset, position.unlocking_duration, @@ -216,7 +215,7 @@ pub(crate) fn withdraw_position( ); // check if the user has pending rewards. Can't withdraw a position without claiming pending rewards first - let rewards_response = query_rewards(deps.as_ref(), info.sender.clone().into_string())?; + let rewards_response = query_rewards(deps.as_ref(), &env, info.sender.clone().into_string())?; match rewards_response { RewardsResponse::RewardsResponse { rewards } => { ensure!(rewards.is_empty(), ContractError::PendingRewards) @@ -256,6 +255,7 @@ pub(crate) fn withdraw_position( if position.open { update_weights( deps.branch(), + &env, &info, &position.lp_asset, position.unlocking_duration, @@ -302,6 +302,7 @@ pub(crate) fn withdraw_position( /// Updates the weights when managing a position. Computes what the weight is gonna be in the next epoch. fn update_weights( deps: DepsMut, + env: &Env, receiver: &MessageInfo, lp_asset: &Coin, unlocking_duration: u64, @@ -315,7 +316,8 @@ fn update_weights( let weight = calculate_weight(lp_asset, unlocking_duration)?; - let (_, mut lp_weight) = get_latest_lp_weight(deps.storage, &lp_asset.denom)?; + let (_, mut lp_weight) = + get_latest_address_weight(deps.storage, &env.contract.address, &lp_asset.denom)?; if fill { // filling position @@ -325,9 +327,14 @@ fn update_weights( lp_weight = lp_weight.saturating_sub(weight); } - LP_WEIGHTS_HISTORY.update::<_, StdError>( + // update the LP weight for the contract + LP_WEIGHT_HISTORY.update::<_, StdError>( deps.storage, - (&lp_asset.denom, current_epoch.id + 1u64), + ( + &env.contract.address, + &lp_asset.denom, + current_epoch.id + 1u64, + ), |_| Ok(lp_weight), )?; @@ -344,7 +351,7 @@ fn update_weights( } //todo if the address weight is zero, remove it from the storage? - ADDRESS_LP_WEIGHT_HISTORY.update::<_, StdError>( + LP_WEIGHT_HISTORY.update::<_, StdError>( deps.storage, (&receiver.sender, &lp_asset.denom, current_epoch.id + 1u64), |_| Ok(address_lp_weight), diff --git a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs index fdc1cdc09..1b1d1d7b9 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/helpers.rs @@ -2,7 +2,7 @@ use cosmwasm_std::{Addr, Coin, Decimal256, Order, StdError, Storage, Uint128}; use white_whale_std::incentive_manager::{Config, EpochId}; -use crate::state::{ADDRESS_LP_WEIGHT_HISTORY, LP_WEIGHTS_HISTORY}; +use crate::state::LP_WEIGHT_HISTORY; use crate::ContractError; const SECONDS_IN_DAY: u64 = 86400; @@ -62,7 +62,7 @@ pub fn get_latest_address_weight( address: &Addr, lp_denom: &str, ) -> Result<(EpochId, Uint128), ContractError> { - let result = ADDRESS_LP_WEIGHT_HISTORY + let result = LP_WEIGHT_HISTORY .prefix((address, lp_denom)) .range(storage, None, None, Order::Descending) .take(1usize) @@ -73,22 +73,6 @@ pub fn get_latest_address_weight( return_latest_weight(result) } -/// Gets the latest available weight snapshot recorded for the given lp. -pub fn get_latest_lp_weight( - storage: &dyn Storage, - lp_denom: &str, -) -> Result<(EpochId, Uint128), ContractError> { - let result = LP_WEIGHTS_HISTORY - .prefix(lp_denom) - .range(storage, None, None, Order::Descending) - .take(1usize) - // take only one item, the last item. Since it's being sorted in descending order, it's the latest one. - .next() - .transpose(); - - return_latest_weight(result) -} - /// Helper function to return the weight from the result. If the result is None, i.e. the weight /// was not found in the map, it returns (0, 0). fn return_latest_weight( diff --git a/contracts/liquidity_hub/incentive-manager/src/queries.rs b/contracts/liquidity_hub/incentive-manager/src/queries.rs index ffd613a66..8f231b9a1 100644 --- a/contracts/liquidity_hub/incentive-manager/src/queries.rs +++ b/contracts/liquidity_hub/incentive-manager/src/queries.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::Deps; +use cosmwasm_std::{Deps, Env}; use white_whale_std::coin::aggregate_coins; use white_whale_std::incentive_manager::{ @@ -9,7 +9,7 @@ use white_whale_std::incentive_manager::{ use crate::incentive::commands::calculate_rewards; use crate::state::{ get_incentive_by_identifier, get_incentives, get_incentives_by_incentive_asset, - get_incentives_by_lp_denom, get_positions_by_receiver, CONFIG, LP_WEIGHTS_HISTORY, + get_incentives_by_lp_denom, get_positions_by_receiver, CONFIG, LP_WEIGHT_HISTORY, }; use crate::ContractError; @@ -61,7 +61,11 @@ pub(crate) fn query_positions( } /// Queries the rewards for a given address. -pub(crate) fn query_rewards(deps: Deps, address: String) -> Result { +pub(crate) fn query_rewards( + deps: Deps, + env: &Env, + address: String, +) -> Result { let receiver = deps.api.addr_validate(&address)?; // check if the user has any open LP positions let open_positions = @@ -82,7 +86,7 @@ pub(crate) fn query_rewards(deps: Deps, address: String) -> Result { total_rewards.append(&mut rewards.clone()) @@ -99,11 +103,15 @@ pub(crate) fn query_rewards(deps: Deps, address: String) -> Result Result { - let lp_weight = LP_WEIGHTS_HISTORY - .may_load(deps.storage, (denom.as_str(), epoch_id))? + let lp_weight = LP_WEIGHT_HISTORY + .may_load( + deps.storage, + (&deps.api.addr_validate(&address)?, denom.as_str(), epoch_id), + )? .ok_or(ContractError::LpWeightNotFound { epoch_id })?; Ok(LpWeightResponse { diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 8798adbe6..d6430e945 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -41,12 +41,10 @@ impl<'a> IndexList for PositionIndexes<'a> { /// The last epoch an address claimed rewards pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch"); -/// The history of total weight (sum of all individual weights) of an LP asset at a given epoch -pub const LP_WEIGHTS_HISTORY: Map<(&str, EpochId), Uint128> = Map::new("lp_weights_history"); - -//todo add the lp denom here as well, otherwise there's no way to distinguish -/// The address lp weight history, i.e. how much lp weight an address had at a given epoch -pub const ADDRESS_LP_WEIGHT_HISTORY: Map<(&Addr, &str, EpochId), Uint128> = +/// The lp weight history for addresses, including the contract. i.e. how much lp weight an address +/// or contract has at a given epoch. +/// Key is a tuple of (address, lp_denom, epoch_id), value is the lp weight. +pub const LP_WEIGHT_HISTORY: Map<(&Addr, &str, EpochId), Uint128> = Map::new("address_lp_weight_history"); /// An monotonically increasing counter to generate unique incentive identifiers. @@ -214,7 +212,7 @@ pub fn get_earliest_address_lp_weight( address: &Addr, lp_denom: &str, ) -> Result<(EpochId, Uint128), ContractError> { - let earliest_weight_history_result = ADDRESS_LP_WEIGHT_HISTORY + let earliest_weight_history_result = LP_WEIGHT_HISTORY .prefix((address, lp_denom)) .range(storage, None, None, Order::Ascending) .next() @@ -228,13 +226,14 @@ pub fn get_earliest_address_lp_weight( } /// Gets the latest entry of an address in the address lp weight history. -/// If the address has no open positions, it returns an error. +/// If the address has no open positions, returns 0 for the weight. pub fn get_latest_address_lp_weight( storage: &dyn Storage, address: &Addr, lp_denom: &str, + epoch_id: &EpochId, ) -> Result<(EpochId, Uint128), ContractError> { - let latest_weight_history_result = ADDRESS_LP_WEIGHT_HISTORY + let latest_weight_history_result = LP_WEIGHT_HISTORY .prefix((address, lp_denom)) .range(storage, None, None, Order::Descending) .next() @@ -242,29 +241,7 @@ pub fn get_latest_address_lp_weight( match latest_weight_history_result { Ok(Some(item)) => Ok(item), - Ok(None) => Err(ContractError::NoOpenPositions), - Err(std_err) => Err(std_err.into()), - } -} - -/// Gets the latest entry of the LP_WEIGHT_HISTORY for the given lp denom. -/// If there's no LP weight history for the given lp denom, i.e. nobody opened a position ever before, -/// it returns 0 for the weight. -pub fn get_latest_lp_weight_record( - storage: &dyn Storage, - lp_denom: &str, - epoch_id: EpochId, -) -> Result<(EpochId, Uint128), ContractError> { - let latest_weight_history_result = LP_WEIGHTS_HISTORY - .prefix(lp_denom) - .range(storage, None, None, Order::Descending) - .next() - .transpose(); - - match latest_weight_history_result { - Ok(Some(item)) => Ok(item), - // if the lp weight was not found in the map, it returns 0 for the weight. - Ok(None) => Ok((epoch_id, Uint128::zero())), + Ok(None) => Ok((epoch_id.to_owned(), Uint128::zero())), Err(std_err) => Err(std_err.into()), } } diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index b17b892be..9b0bc2b40 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -518,6 +518,7 @@ impl TestingSuite { let rewards_response: StdResult = self.app.wrap().query_wasm_smart( &self.incentive_manager_addr, &white_whale_std::incentive_manager::QueryMsg::LPWeight { + address: self.incentive_manager_addr.to_string(), denom: denom.to_string(), epoch_id, }, diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index 24df701cd..ce3f4cad0 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -108,6 +108,8 @@ pub enum QueryMsg { /// Retrieves the total LP weight in the contract for a given denom on a given epoch. #[returns(LpWeightResponse)] LPWeight { + /// The address to get the LP weight for. + address: String, /// The denom to get the total LP weight for. denom: String, /// The epoch id to get the LP weight for. From 7d79a36f483df0d1d7f9bb9ad08b97bb6733fcc6 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 11 Apr 2024 15:38:48 +0100 Subject: [PATCH 32/35] chore: add incentive manager to xtask --- xtask/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 60540c5af..2864d6e63 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -63,6 +63,7 @@ pub mod tasks { generate_schema!("fee_collector", fee_collector), generate_schema!("fee_distributor", fee_distributor), generate_schema!("pool-manager", pool_manager), + generate_schema!("incentive-manager", incentive_manager), generate_schema!("frontend-helper", frontend_helper), generate_schema!("incentive", incentive), generate_schema!("incentive-factory", incentive_factory), From 88f82b1f996e775991b031eb601c4f32e8de3e1d Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 11 Apr 2024 16:04:19 +0100 Subject: [PATCH 33/35] chore: fix injective build --- .../terraswap_pair/src/migrations.rs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs b/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs index 18806e632..276ee5ffa 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs +++ b/contracts/liquidity_hub/pool-network/terraswap_pair/src/migrations.rs @@ -194,23 +194,8 @@ pub fn migrate_to_v13x(deps: DepsMut) -> Result<(), StdError> { pub swap_fee: Fee, } - const CONFIG_V110: Item = Item::new("leaderboard"); - let leaderboard = CONFIG_V110.load(deps.storage)?; - - let mut start_from: Option = None; - for (addr, amount) in leaderboard.iter() { - let leaderboard = deps.api.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "guppy_furnace".to_string(), - msg: to_binary(&LeaderBoard { - start_from: start_from, - limit: 30, - })?, - }))?; - - LEADERBOARD.save(deps.storage, &"uguppy", &leaderboard)?; - - start_from = Some(leaderboard.last()?); - } + const CONFIG_V110: Item = Item::new("config"); + let config_v110 = CONFIG_V110.load(deps.storage)?; // Add burn fee to config. Zero fee is used as default. let config = Config { @@ -297,7 +282,6 @@ pub fn migrate_to_v13x(deps: DepsMut) -> Result<(), StdError> { Ok(()) } - /// This migration adds the `cosmwasm_pool_interface` to the config, so we can see if the swap is coming from /// the osmosis pool manager or not in order to pay the osmosis taker fee. #[cfg(feature = "osmosis")] From fb30f1f6f7a0a0c93e2cf8576b927feb00ffd801 Mon Sep 17 00:00:00 2001 From: Kerber0x <94062656+kerber0x@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:06:01 +0100 Subject: [PATCH 34/35] chore: remove the schema generation alias Co-authored-by: kaimen-sano --- contracts/liquidity_hub/incentive-manager/.cargo/config | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/liquidity_hub/incentive-manager/.cargo/config b/contracts/liquidity_hub/incentive-manager/.cargo/config index af5698e58..4f96ce061 100644 --- a/contracts/liquidity_hub/incentive-manager/.cargo/config +++ b/contracts/liquidity_hub/incentive-manager/.cargo/config @@ -1,4 +1,3 @@ [alias] wasm = "build --release --lib --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --bin schema" From 64ec60fb21b5f7bdb2184b87891fb73ad5489197 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 12 Apr 2024 14:46:26 +0100 Subject: [PATCH 35/35] chore: cleanup things after PR suggestions --- .../incentive-manager/src/bin/schema.rs | 11 ----------- .../incentive-manager/src/helpers.rs | 3 +-- .../incentive-manager/src/manager/commands.rs | 15 +++++++++------ .../pool-manager/src/manager/commands.rs | 1 + packages/white-whale-std/src/coin.rs | 16 +++++++++++----- scripts/deployment/deploy_env/base.env | 0 scripts/deployment/deploy_env/base_migaloo.env | 0 7 files changed, 22 insertions(+), 24 deletions(-) delete mode 100644 contracts/liquidity_hub/incentive-manager/src/bin/schema.rs delete mode 100644 scripts/deployment/deploy_env/base.env delete mode 100644 scripts/deployment/deploy_env/base_migaloo.env diff --git a/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs b/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs deleted file mode 100644 index 269701f56..000000000 --- a/contracts/liquidity_hub/incentive-manager/src/bin/schema.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_schema::write_api; - -use white_whale_std::incentive_manager::{ExecuteMsg, InstantiateMsg, QueryMsg}; - -fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } -} diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index c8c0a6d63..997bf7bc9 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -14,7 +14,7 @@ pub(crate) fn process_incentive_creation_fee( config: &Config, info: &MessageInfo, incentive_creation_fee: &Coin, - params: &mut IncentiveParams, + params: &IncentiveParams, ) -> Result, ContractError> { let mut messages: Vec = vec![]; @@ -80,7 +80,6 @@ pub(crate) fn process_incentive_creation_fee( } /// Asserts the incentive asset was sent correctly, considering the incentive creation fee if applicable. -/// Returns a vector of messages to be sent (applies only when the incentive asset is a CW20 token) pub(crate) fn assert_incentive_asset( info: &MessageInfo, incentive_creation_fee: &Coin, diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 8aef989c6..04c568831 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -3,10 +3,11 @@ use cosmwasm_std::{ Storage, Uint128, Uint64, }; -use white_whale_std::coin::{get_subdenom, is_factory_token}; +use white_whale_std::coin::{get_factory_token_subdenom, is_factory_token}; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; use white_whale_std::incentive_manager::MIN_INCENTIVE_AMOUNT; use white_whale_std::incentive_manager::{Curve, Incentive, IncentiveParams}; +use white_whale_std::lp_common::LP_SYMBOL; use crate::helpers::{ assert_incentive_asset, process_incentive_creation_fee, validate_emergency_unlock_penalty, @@ -43,7 +44,7 @@ pub(crate) fn fill_incentive( fn create_incentive( deps: DepsMut, info: MessageInfo, - mut params: IncentiveParams, + params: IncentiveParams, ) -> Result { // check if there are any expired incentives for this LP asset let config = CONFIG.load(deps.storage)?; @@ -86,7 +87,7 @@ fn create_incentive( } ); - let incentive_creation_fee = config.create_incentive_fee.clone(); + let incentive_creation_fee = config.clone().create_incentive_fee; if incentive_creation_fee.amount != Uint128::zero() { // verify the fee to create an incentive is being paid @@ -94,7 +95,7 @@ fn create_incentive( &config, &info, &incentive_creation_fee, - &mut params, + ¶ms, )?); } @@ -304,8 +305,10 @@ pub(crate) fn on_epoch_changed( .into_iter() .filter(|asset| { if is_factory_token(asset.denom.as_str()) { - //todo remove this hardcoded uLP and point to the pool manager const, to be moved to the white-whale-std package - get_subdenom(asset.denom.as_str()) == "uLP" + match get_factory_token_subdenom(asset.denom.as_str()) { + Ok(subdenom) => subdenom == LP_SYMBOL, + Err(_) => false, + } } else { false } diff --git a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs index 431cd2102..ca96fb52a 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -172,6 +172,7 @@ pub fn create_pair( let lp_symbol = format!("{pair_label}.pool.{identifier}.{LP_SYMBOL}"); let lp_asset = format!("{}/{}/{}", "factory", env.contract.address, lp_symbol); + #[allow(clippy::redundant_clone)] PAIRS.save( deps.storage, &identifier, diff --git a/packages/white-whale-std/src/coin.rs b/packages/white-whale-std/src/coin.rs index 7a89b5e72..5ca1a1dd1 100644 --- a/packages/white-whale-std/src/coin.rs +++ b/packages/white-whale-std/src/coin.rs @@ -102,11 +102,17 @@ pub fn is_factory_token(denom: &str) -> bool { } /// Gets the subdenom of a factory token. To be called after [is_factory_token] has been successful. -pub fn get_subdenom(denom: &str) -> &str { - denom - .splitn(3, '/') - .nth(2) - .expect("Expected at least three elements") +pub fn get_factory_token_subdenom(denom: &str) -> StdResult<&str> { + let subdenom = denom.splitn(3, '/').nth(2); + + subdenom.map_or_else( + || { + Err(StdError::generic_err( + "Splitting factory token subdenom failed", + )) + }, + Ok, + ) } /// Builds the label for a factory token denom in such way that it returns a label like "factory/mig...xyz/123...456". diff --git a/scripts/deployment/deploy_env/base.env b/scripts/deployment/deploy_env/base.env deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/deployment/deploy_env/base_migaloo.env b/scripts/deployment/deploy_env/base_migaloo.env deleted file mode 100644 index e69de29bb..000000000