diff --git a/Cargo.lock b/Cargo.lock index 0bf3a24a..f99f8b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,6 +961,8 @@ dependencies = [ "cw2", "cw20", "cw20-base", + "epoch-manager", + "incentive-manager", "semver", "serde", "sha2 0.10.8", diff --git a/Cargo.toml b/Cargo.toml index 709346c1..013aeb68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,8 @@ 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" } +incentive-manager = { path = "./contracts/liquidity_hub/incentive-manager" } [workspace.metadata.dylint] libraries = [{ git = "https://github.com/0xFable/cw-lint" }] diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index f50e8a64..d27880f3 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -43,7 +43,7 @@ pub fn instantiate( 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)?, + whale_lair_addr: deps.api.addr_validate(&msg.bonding_manager_addr)?, create_incentive_fee: msg.create_incentive_fee, max_concurrent_incentives: msg.max_concurrent_incentives, max_incentive_epoch_buffer: msg.max_incentive_epoch_buffer, diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index 1b319a41..f34290ce 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -201,7 +201,7 @@ impl TestingSuite { #[allow(clippy::too_many_arguments)] pub(crate) fn instantiate( &mut self, - whale_lair_addr: String, + bonding_manager_addr: String, epoch_manager_addr: String, create_incentive_fee: Coin, max_concurrent_incentives: u32, @@ -213,7 +213,7 @@ impl TestingSuite { let msg = InstantiateMsg { owner: self.creator().to_string(), epoch_manager_addr, - whale_lair_addr, + bonding_manager_addr, create_incentive_fee, max_concurrent_incentives, max_incentive_epoch_buffer, @@ -244,7 +244,7 @@ impl TestingSuite { #[allow(clippy::too_many_arguments)] pub(crate) fn instantiate_err( &mut self, - whale_lair_addr: String, + bonding_manager_addr: String, epoch_manager_addr: String, create_incentive_fee: Coin, max_concurrent_incentives: u32, @@ -257,7 +257,7 @@ impl TestingSuite { let msg = InstantiateMsg { owner: self.creator().to_string(), epoch_manager_addr, - whale_lair_addr, + bonding_manager_addr, create_incentive_fee, max_concurrent_incentives, max_incentive_epoch_buffer, diff --git a/contracts/liquidity_hub/pool-manager/Cargo.toml b/contracts/liquidity_hub/pool-manager/Cargo.toml index de0bd39c..81032eca 100644 --- a/contracts/liquidity_hub/pool-manager/Cargo.toml +++ b/contracts/liquidity_hub/pool-manager/Cargo.toml @@ -54,4 +54,6 @@ cw-multi-test.workspace = true anyhow.workspace = true test-case.workspace = true whale-lair.workspace = true +incentive-manager.workspace = true +epoch-manager.workspace = true white-whale-testing.workspace = true diff --git a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json index f937bcb3..2c93551f 100644 --- a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json +++ b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json @@ -7,11 +7,15 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "fee_collector_addr", + "bonding_manager_addr", + "incentive_manager_addr", "pool_creation_fee" ], "properties": { - "fee_collector_addr": { + "bonding_manager_addr": { + "type": "string" + }, + "incentive_manager_addr": { "type": "string" }, "pool_creation_fee": { @@ -105,6 +109,13 @@ "pair_identifier" ], "properties": { + "lock_position_identifier": { + "description": "The identifier of the position to lock the LP tokens in the incentive manager, if any.", + "type": [ + "string", + "null" + ] + }, "pair_identifier": { "type": "string" }, @@ -123,6 +134,15 @@ "type": "null" } ] + }, + "unlocking_duration": { + "description": "The amount of time in seconds to unlock tokens if taking part on the incentives. If not passed, the tokens will not be locked and the LP tokens will be returned to the user.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -1033,19 +1053,33 @@ "Config": { "type": "object", "required": [ + "bonding_manager_addr", "feature_toggle", - "pool_creation_fee", - "whale_lair_addr" + "incentive_manager_addr", + "pool_creation_fee" ], "properties": { + "bonding_manager_addr": { + "description": "The address of the bonding manager contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "feature_toggle": { "$ref": "#/definitions/FeatureToggle" }, + "incentive_manager_addr": { + "description": "The address of the incentive manager contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "pool_creation_fee": { "$ref": "#/definitions/Coin" - }, - "whale_lair_addr": { - "$ref": "#/definitions/Addr" } }, "additionalProperties": false diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json index ccbbc66e..44e27ae1 100644 --- a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json @@ -62,6 +62,13 @@ "pair_identifier" ], "properties": { + "lock_position_identifier": { + "description": "The identifier of the position to lock the LP tokens in the incentive manager, if any.", + "type": [ + "string", + "null" + ] + }, "pair_identifier": { "type": "string" }, @@ -80,6 +87,15 @@ "type": "null" } ] + }, + "unlocking_duration": { + "description": "The amount of time in seconds to unlock tokens if taking part on the incentives. If not passed, the tokens will not be locked and the LP tokens will be returned to the user.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/instantiate.json b/contracts/liquidity_hub/pool-manager/schema/raw/instantiate.json index e87e515e..8fdc4795 100644 --- a/contracts/liquidity_hub/pool-manager/schema/raw/instantiate.json +++ b/contracts/liquidity_hub/pool-manager/schema/raw/instantiate.json @@ -3,11 +3,15 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "fee_collector_addr", + "bonding_manager_addr", + "incentive_manager_addr", "pool_creation_fee" ], "properties": { - "fee_collector_addr": { + "bonding_manager_addr": { + "type": "string" + }, + "incentive_manager_addr": { "type": "string" }, "pool_creation_fee": { diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/response_to_config.json b/contracts/liquidity_hub/pool-manager/schema/raw/response_to_config.json index 7284764a..b1b907f1 100644 --- a/contracts/liquidity_hub/pool-manager/schema/raw/response_to_config.json +++ b/contracts/liquidity_hub/pool-manager/schema/raw/response_to_config.json @@ -34,19 +34,33 @@ "Config": { "type": "object", "required": [ + "bonding_manager_addr", "feature_toggle", - "pool_creation_fee", - "whale_lair_addr" + "incentive_manager_addr", + "pool_creation_fee" ], "properties": { + "bonding_manager_addr": { + "description": "The address of the bonding manager contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "feature_toggle": { "$ref": "#/definitions/FeatureToggle" }, + "incentive_manager_addr": { + "description": "The address of the incentive manager contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "pool_creation_fee": { "$ref": "#/definitions/Coin" - }, - "whale_lair_addr": { - "$ref": "#/definitions/Addr" } }, "additionalProperties": false diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index 535e8bf6..d41d35b4 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -27,7 +27,8 @@ pub fn instantiate( ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let config: Config = Config { - whale_lair_addr: deps.api.addr_validate(&msg.fee_collector_addr)?, + bonding_manager_addr: deps.api.addr_validate(&msg.bonding_manager_addr)?, + incentive_manager_addr: deps.api.addr_validate(&msg.incentive_manager_addr)?, // We must set a creation fee on instantiation to prevent spamming of pools pool_creation_fee: msg.pool_creation_fee, feature_toggle: FeatureToggle { @@ -72,6 +73,8 @@ pub fn execute( slippage_tolerance, receiver, pair_identifier, + unlocking_duration, + lock_position_identifier, } => liquidity::commands::provide_liquidity( deps, env, @@ -79,6 +82,8 @@ pub fn execute( slippage_tolerance, receiver, pair_identifier, + unlocking_duration, + lock_position_identifier, ), ExecuteMsg::Swap { offer_asset, diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 6f34f7a6..6f388568 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{ + coins, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, +}; use white_whale_std::pool_network::asset::PairType; use crate::{ @@ -20,9 +22,7 @@ pub const MAX_ASSETS_PER_POOL: usize = 4; // todo allow providing liquidity with a single asset -//todo allow passing an optional locking period for the LP once the liquidity is provided, so tokens -// are locked in the incentive manager - +#[allow(clippy::too_many_arguments)] pub fn provide_liquidity( deps: DepsMut, env: Env, @@ -30,6 +30,8 @@ pub fn provide_liquidity( slippage_tolerance: Option, receiver: Option, pair_identifier: String, + unlocking_duration: Option, + lock_position_identifier: Option, ) -> Result { let config = MANAGER_CONFIG.load(deps.storage)?; // check if the deposit feature is enabled @@ -145,12 +147,40 @@ pub fn provide_liquidity( // mint LP token to sender let receiver = receiver.unwrap_or_else(|| info.sender.to_string()); - messages.push(white_whale_std::lp_common::mint_lp_token_msg( - liquidity_token, - &info.sender, - &env.contract.address, - share, - )?); + // if the unlocking duration is set, lock the LP tokens in the incentive manager + if let Some(unlocking_duration) = unlocking_duration { + // mint the lp tokens to the contract + messages.push(white_whale_std::lp_common::mint_lp_token_msg( + liquidity_token.clone(), + &env.contract.address, + &env.contract.address, + share, + )?); + + // lock the lp tokens in the incentive manager on behalf of the receiver + messages.push( + wasm_execute( + config.incentive_manager_addr, + &white_whale_std::incentive_manager::ExecuteMsg::ManagePosition { + action: white_whale_std::incentive_manager::PositionAction::Fill { + identifier: lock_position_identifier, + unlocking_duration, + receiver: Some(receiver.clone()), + }, + }, + coins(share.u128(), liquidity_token), + )? + .into(), + ); + } else { + // if not, just mint the LP tokens to the receiver + messages.push(white_whale_std::lp_common::mint_lp_token_msg( + liquidity_token, + &info.sender, + &env.contract.address, + share, + )?); + } pair.assets = pool_assets.clone(); PAIRS.save(deps.storage, &pair_identifier, &pair)?; diff --git a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs index 95654699..1bcb08a1 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -110,7 +110,7 @@ pub fn create_pair( // send pair creation fee to whale lair i.e the new fee_collector messages.push(fill_rewards_msg_coin( - config.whale_lair_addr.into_string(), + config.bonding_manager_addr.into_string(), creation_fee, )?); diff --git a/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs b/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs index 5f4d2fbf..5bc62aca 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs @@ -16,7 +16,7 @@ pub fn update_config( MANAGER_CONFIG.update(deps.storage, |mut config| { if let Some(whale_lair_addr) = whale_lair_addr { let whale_lair_addr = deps.api.addr_validate(&whale_lair_addr)?; - config.whale_lair_addr = whale_lair_addr; + config.bonding_manager_addr = whale_lair_addr; } if let Some(pool_creation_fee) = pool_creation_fee { diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index 724dd493..c91013f3 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -118,7 +118,7 @@ pub fn execute_swap_operations( } if !swap_result.protocol_fee_asset.amount.is_zero() { fee_messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: config.whale_lair_addr.to_string(), + contract_addr: config.bonding_manager_addr.to_string(), msg: to_json_binary(&whale_lair::ExecuteMsg::FillRewardsCoin { })?, funds: vec![swap_result.protocol_fee_asset.clone()], @@ -128,7 +128,7 @@ pub fn execute_swap_operations( // todo remove, the swap_fee_asset stays in the pool if !swap_result.swap_fee_asset.amount.is_zero() { fee_messages.push(CosmosMsg::Bank(BankMsg::Send { - to_address: config.whale_lair_addr.to_string(), + to_address: config.bonding_manager_addr.to_string(), amount: vec![swap_result.swap_fee_asset], })); } diff --git a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs index 4afdba5f..5d952b52 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -63,7 +63,7 @@ pub fn swap( } if !swap_result.protocol_fee_asset.amount.is_zero() { messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: config.whale_lair_addr.to_string(), + contract_addr: config.bonding_manager_addr.to_string(), msg: to_json_binary(&whale_lair::ExecuteMsg::FillRewardsCoin { })?, funds: vec![swap_result.protocol_fee_asset.clone()], @@ -73,7 +73,7 @@ pub fn swap( // todo remove, this stays within the pool if !swap_result.swap_fee_asset.amount.is_zero() { messages.push(CosmosMsg::Bank(BankMsg::Send { - to_address: config.whale_lair_addr.to_string(), + to_address: config.bonding_manager_addr.to_string(), amount: vec![swap_result.swap_fee_asset.clone()], })); } diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index a24a1060..7fc2e991 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -1,9 +1,11 @@ -use crate::ContractError; use cosmwasm_std::{coin, Addr, Coin, Decimal, Uint128}; + use white_whale_std::fee::Fee; use white_whale_std::fee::PoolFee; use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; +use crate::ContractError; + use super::suite::TestingSuite; // Using our suite lets test create pair @@ -13,7 +15,7 @@ use super::suite::TestingSuite; fn instantiate_normal() { let mut suite = TestingSuite::default_with_balances(vec![]); - suite.instantiate(suite.senders[0].to_string()); + suite.instantiate(suite.senders[0].to_string(), suite.senders[1].to_string()); } // add features `token_factory` so tests are compiled using the correct flag @@ -85,6 +87,8 @@ fn deposit_and_withdraw_sanity_check() { .provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -167,8 +171,8 @@ fn deposit_and_withdraw_sanity_check() { } mod pair_creation_failures { - use super::*; + // Insufficient fee to create pair; 90 instead of 100 #[test] fn insufficient_pair_creation_fee() { @@ -320,6 +324,7 @@ mod router { use white_whale_std::pool_manager::{SwapRoute, SwapRouteCreatorResponse}; use super::*; + #[test] fn basic_swap_operations_test() { let mut suite = TestingSuite::default_with_balances(vec![ @@ -397,6 +402,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -418,6 +425,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -482,21 +491,21 @@ mod router { // ensure that fees got sent to the appropriate place suite.query_balance( - suite.whale_lair_addr.to_string(), + suite.bonding_manager_addr.to_string(), "uusd".to_string(), |amt| { assert_eq!(amt.unwrap().amount.u128(), 2000 + 4 * 2); }, ); suite.query_balance( - suite.whale_lair_addr.to_string(), + suite.bonding_manager_addr.to_string(), "uwhale".to_string(), |amt| { assert_eq!(amt.unwrap().amount.u128(), 0); }, ); suite.query_balance( - suite.whale_lair_addr.to_string(), + suite.bonding_manager_addr.to_string(), "uluna".to_string(), |amt| { assert_eq!(amt.unwrap().amount.u128(), 4 * 2); @@ -584,6 +593,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -605,6 +616,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -721,6 +734,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -743,6 +758,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -788,7 +805,7 @@ mod router { result.unwrap_err().downcast_ref::(), Some(&ContractError::NonConsecutiveSwapOperations { previous_output: "uluna".to_string(), - next_input: "uwhale".to_string() + next_input: "uwhale".to_string(), }) ); }, @@ -876,6 +893,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -897,6 +916,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1096,6 +1117,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1117,6 +1140,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1172,7 +1197,7 @@ mod router { result.unwrap_err().downcast_ref::(), Some(&ContractError::MinimumReceiveAssertion { minimum_receive: Uint128::new(975), - swap_amount: Uint128::new(974) + swap_amount: Uint128::new(974), }) ) }, @@ -1253,6 +1278,8 @@ mod router { .provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1270,6 +1297,8 @@ mod router { .provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1411,6 +1440,8 @@ mod router { .provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1428,6 +1459,8 @@ mod router { .provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1604,6 +1637,8 @@ mod swapping { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1815,6 +1850,8 @@ mod swapping { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2016,6 +2053,8 @@ mod swapping { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2092,7 +2131,7 @@ mod swapping { // Verify fee collection by querying the address of the whale lair and checking its balance // Should be 297 uLUNA suite.query_balance( - suite.whale_lair_addr.to_string(), + suite.bonding_manager_addr.to_string(), "uluna".to_string(), |result| { assert_eq!(result.unwrap().amount, Uint128::from(297u128)); @@ -2225,8 +2264,388 @@ mod ownership { ); let config = suite.query_config(); - assert_ne!(config.whale_lair_addr, initial_config.whale_lair_addr); + assert_ne!( + config.bonding_manager_addr, + initial_config.bonding_manager_addr + ); assert_ne!(config.pool_creation_fee, initial_config.pool_creation_fee); assert_ne!(config.feature_toggle, initial_config.feature_toggle); } } + +mod locking_lp { + use cosmwasm_std::{coin, Coin, Decimal, Uint128}; + + use white_whale_std::fee::{Fee, PoolFee}; + use white_whale_std::incentive_manager::Position; + use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; + + use crate::tests::suite::TestingSuite; + + #[test] + fn provide_liquidity_locking_lp_no_lock_position_identifier() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(10_000_000u128, "uwhale".to_string()), + coin(10_000_000u128, "uluna".to_string()), + coin(10_000u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + + // Asset denoms with uwhale and uluna + let asset_denoms = vec!["uwhale".to_string(), "uluna".to_string()]; + + // Default Pool fees white_whale_std::pool_network::pair::PoolFee + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::zero(), + }, + swap_fee: Fee { + share: Decimal::zero(), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::zero(), + }, + swap_fee: Fee { + share: Decimal::zero(), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + // Create a pair + suite.instantiate_default().create_pair( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + let contract_addr = suite.pool_manager_addr.clone(); + let incentive_manager_addr = suite.incentive_manager_addr.clone(); + let lp_denom = suite.get_lp_denom("whale-uluna".to_string()); + + // Lets try to add liquidity + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + Some(86_400u64), + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + // Ensure we got 999_000 in the response which is 1_000_000 less the initial liquidity amount + assert!(result.unwrap().events.iter().any(|event| { + event.attributes.iter().any(|attr| { + attr.key == "share" + && attr.value + == (Uint128::from(1_000_000u128) - MINIMUM_LIQUIDITY_AMOUNT) + .to_string() + }) + })); + }, + ) + .query_all_balances(creator.to_string(), |result| { + let balances = result.unwrap(); + // the lp tokens should have gone to the incentive manager + assert!(!balances + .iter() + .any(|coin| { coin.denom == lp_denom.clone() })); + }) + // contract should have 1_000 LP shares (MINIMUM_LIQUIDITY_AMOUNT) + .query_all_balances(contract_addr.to_string(), |result| { + let balances = result.unwrap(); + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom.clone() && coin.amount == MINIMUM_LIQUIDITY_AMOUNT + })); + }) + // check the LP went to the incentive manager + .query_all_balances(incentive_manager_addr.to_string(), |result| { + let balances = result.unwrap(); + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom && coin.amount == Uint128::from(999_000u128) + })); + }); + + suite.query_incentive_positions(creator.clone(), None, |result| { + let positions = result.unwrap().positions; + assert_eq!(positions.len(), 1); + assert_eq!(positions[0], Position { + identifier: "1".to_string(), + lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + unlocking_duration: 86_400, + open: true, + expiring_at: None, + receiver: creator.clone(), + }); + }); + + // let's do it again, it should create another position on the incentive manager + + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + Some(200_000u64), + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + result.unwrap(); + }, + ) + .query_all_balances(creator.to_string(), |result| { + let balances = result.unwrap(); + // the lp tokens should have gone to the incentive manager + assert!(!balances + .iter() + .any(|coin| { coin.denom == lp_denom.clone() })); + }) + // check the LP went to the incentive manager + .query_all_balances(incentive_manager_addr.to_string(), |result| { + let balances = result.unwrap(); + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom && coin.amount == Uint128::from(1_999_000u128) + })); + }); + + suite.query_incentive_positions(creator.clone(), None, |result| { + let positions = result.unwrap().positions; + assert_eq!(positions.len(), 2); + assert_eq!(positions[0], Position { + identifier: "1".to_string(), + lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + unlocking_duration: 86_400, + open: true, + expiring_at: None, + receiver: creator.clone(), + }); + assert_eq!(positions[1], Position { + identifier: "2".to_string(), + lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_000_000u128) }, + unlocking_duration: 200_000, + open: true, + expiring_at: None, + receiver: creator.clone(), + }); + }); + } + + #[test] + fn provide_liquidity_locking_lp_reusing_position_identifier() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(10_000_000u128, "uwhale".to_string()), + coin(10_000_000u128, "uluna".to_string()), + coin(10_000u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + + // Asset denoms with uwhale and uluna + let asset_denoms = vec!["uwhale".to_string(), "uluna".to_string()]; + + // Default Pool fees white_whale_std::pool_network::pair::PoolFee + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::zero(), + }, + swap_fee: Fee { + share: Decimal::zero(), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::zero(), + }, + swap_fee: Fee { + share: Decimal::zero(), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + // Create a pair + suite.instantiate_default().create_pair( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + let contract_addr = suite.pool_manager_addr.clone(); + let incentive_manager_addr = suite.incentive_manager_addr.clone(); + let lp_denom = suite.get_lp_denom("whale-uluna".to_string()); + + // Lets try to add liquidity + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + Some(86_400u64), + Some("incentive_identifier".to_string()), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + // Ensure we got 999_000 in the response which is 1_000_000 less the initial liquidity amount + assert!(result.unwrap().events.iter().any(|event| { + event.attributes.iter().any(|attr| { + attr.key == "share" + && attr.value + == (Uint128::from(1_000_000u128) - MINIMUM_LIQUIDITY_AMOUNT) + .to_string() + }) + })); + }, + ) + .query_all_balances(creator.to_string(), |result| { + let balances = result.unwrap(); + // the lp tokens should have gone to the incentive manager + assert!(!balances + .iter() + .any(|coin| { coin.denom == lp_denom.clone() })); + }) + // contract should have 1_000 LP shares (MINIMUM_LIQUIDITY_AMOUNT) + .query_all_balances(contract_addr.to_string(), |result| { + let balances = result.unwrap(); + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom.clone() && coin.amount == MINIMUM_LIQUIDITY_AMOUNT + })); + }) + // check the LP went to the incentive manager + .query_all_balances(incentive_manager_addr.to_string(), |result| { + let balances = result.unwrap(); + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom && coin.amount == Uint128::from(999_000u128) + })); + }); + + suite.query_incentive_positions(creator.clone(), None, |result| { + let positions = result.unwrap().positions; + assert_eq!(positions.len(), 1); + assert_eq!(positions[0], Position { + identifier: "incentive_identifier".to_string(), + lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + unlocking_duration: 86_400, + open: true, + expiring_at: None, + receiver: creator.clone(), + }); + }); + + // let's do it again, reusing the same incentive identifier + + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + Some(200_000u64), + Some("incentive_identifier".to_string()), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + result.unwrap(); + }, + ) + .query_all_balances(creator.to_string(), |result| { + let balances = result.unwrap(); + // the lp tokens should have gone to the incentive manager + assert!(!balances + .iter() + .any(|coin| { coin.denom == lp_denom.clone() })); + }) + // check the LP went to the incentive manager + .query_all_balances(incentive_manager_addr.to_string(), |result| { + let balances = result.unwrap(); + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom && coin.amount == Uint128::from(1_999_000u128) + })); + }); + + suite.query_incentive_positions(creator.clone(), None, |result| { + let positions = result.unwrap().positions; + // the position should be updated + assert_eq!(positions.len(), 1); + assert_eq!(positions[0], Position { + identifier: "incentive_identifier".to_string(), + lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_999_000u128) }, + unlocking_duration: 86_400, + open: true, + expiring_at: None, + receiver: creator.clone(), + }); + }); + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 117d402f..a506d092 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -6,18 +6,20 @@ use white_whale_std::pool_manager::{ use white_whale_std::pool_manager::{InstantiateMsg, PairInfo}; 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, Contract, ContractWrapper, DistributionKeeper, Executor, FailingModule, GovFailingModule, IbcFailingModule, StakeKeeper, WasmKeeper, }; + +use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig}; use white_whale_std::fee::PoolFee; +use white_whale_std::incentive_manager::PositionsResponse; +use white_whale_std::lp_common::LP_SYMBOL; use white_whale_std::pool_network::asset::{AssetInfo, PairType}; use white_whale_std::pool_network::pair::{ReverseSimulationResponse, SimulationResponse}; use white_whale_testing::multi_test::stargate_mock::StargateMock; -use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; -use white_whale_std::lp_common::LP_SYMBOL; - fn contract_pool_manager() -> Box> { let contract = ContractWrapper::new_with_empty( crate::contract::execute, @@ -29,7 +31,7 @@ fn contract_pool_manager() -> Box> { } /// Creates the whale lair contract -pub fn whale_lair_contract() -> Box> { +pub fn bonding_manager_contract() -> Box> { let contract = ContractWrapper::new( whale_lair::contract::execute, whale_lair::contract::instantiate, @@ -40,6 +42,30 @@ pub fn whale_lair_contract() -> Box> { 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(whale_lair::contract::migrate); + + Box::new(contract) +} + +/// 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(whale_lair::contract::migrate); + + Box::new(contract) +} + type OsmosisTokenFactoryApp = App< BankKeeper, MockApiBech32, @@ -56,8 +82,10 @@ type OsmosisTokenFactoryApp = App< pub struct TestingSuite { app: OsmosisTokenFactoryApp, pub senders: [Addr; 3], - pub whale_lair_addr: Addr, + pub bonding_manager_addr: Addr, pub pool_manager_addr: Addr, + pub incentive_manager_addr: Addr, + pub epoch_manager_addr: Addr, pub cw20_tokens: Vec, } @@ -113,16 +141,23 @@ impl TestingSuite { Self { app, senders: [sender_1, sender_2, sender_3], - whale_lair_addr: Addr::unchecked(""), + bonding_manager_addr: Addr::unchecked(""), pool_manager_addr: Addr::unchecked(""), + incentive_manager_addr: Addr::unchecked(""), + epoch_manager_addr: Addr::unchecked(""), cw20_tokens: vec![], } } #[track_caller] - pub(crate) fn instantiate(&mut self, whale_lair_addr: String) -> &mut Self { + pub(crate) fn instantiate( + &mut self, + bonding_manager_addr: String, + incentive_manager_addr: String, + ) -> &mut Self { let msg = InstantiateMsg { - fee_collector_addr: whale_lair_addr, + bonding_manager_addr, + incentive_manager_addr, pool_creation_fee: coin(1_000, "uusd"), }; @@ -147,19 +182,25 @@ impl TestingSuite { #[track_caller] pub(crate) fn instantiate_default(&mut self) -> &mut Self { - self.create_whale_lair(); + self.create_bonding_manager(); + self.create_epoch_manager(); + self.create_incentive_manager(); - // 17 May 2023 17:00:00 UTC - let timestamp = Timestamp::from_seconds(1684342800u64); + // 25 April 2024 15:00:00 UTC + let timestamp = Timestamp::from_seconds(1714057200); self.set_time(timestamp); - self.instantiate(self.whale_lair_addr.to_string()) + self.instantiate( + self.bonding_manager_addr.to_string(), + self.incentive_manager_addr.to_string(), + ) } - fn create_whale_lair(&mut self) { - let whale_lair_id = self.app.store_code(whale_lair_contract()); + fn create_bonding_manager(&mut self) { + let bonding_manager_id = self.app.store_code(bonding_manager_contract()); // create whale lair + // todo replace with bonding manager InstantiateMsg let msg = white_whale_std::whale_lair::InstantiateMsg { unbonding_period: Uint64::new(86400u64), growth_rate: Decimal::one(), @@ -175,14 +216,76 @@ impl TestingSuite { let creator = self.creator().clone(); - self.whale_lair_addr = self + self.bonding_manager_addr = self .app .instantiate_contract( - whale_lair_id, + bonding_manager_id, creator.clone(), &msg, &[], - "White Whale Lair".to_string(), + "Bonding Manager".to_string(), + Some(creator.to_string()), + ) + .unwrap(); + } + fn create_epoch_manager(&mut self) { + let epoch_manager_id = self.app.store_code(epoch_manager_contract()); + + let msg = white_whale_std::epoch_manager::epoch_manager::InstantiateMsg { + start_epoch: Epoch { + id: 0, + start_time: Timestamp::from_seconds(1714057200), + }, + epoch_config: EpochConfig { + duration: Uint64::new(86_400_000000000), + genesis_epoch: Uint64::new(1714057200_000000000), + }, + }; + + let creator = self.creator().clone(); + + self.epoch_manager_addr = self + .app + .instantiate_contract( + epoch_manager_id, + creator.clone(), + &msg, + &[], + "Epoch Manager".to_string(), + Some(creator.to_string()), + ) + .unwrap(); + } + fn create_incentive_manager(&mut self) { + let incentive_manager_id = self.app.store_code(incentive_manager_contract()); + + let creator = self.creator().clone(); + let epoch_manager_addr = self.epoch_manager_addr.to_string(); + let bonding_manager_addr = self.bonding_manager_addr.to_string(); + + let msg = white_whale_std::incentive_manager::InstantiateMsg { + owner: creator.clone().to_string(), + epoch_manager_addr, + bonding_manager_addr, + create_incentive_fee: Coin { + denom: "uwhale".to_string(), + amount: Uint128::zero(), + }, + max_concurrent_incentives: 5, + max_incentive_epoch_buffer: 014, + min_unlocking_duration: 86_400, + max_unlocking_duration: 31_536_000, + emergency_unlock_penalty: Decimal::percent(10), + }; + + self.incentive_manager_addr = self + .app + .instantiate_contract( + incentive_manager_id, + creator.clone(), + &msg, + &[], + "Incentive Manager".to_string(), Some(creator.to_string()), ) .unwrap(); @@ -213,6 +316,8 @@ impl TestingSuite { &mut self, sender: Addr, pair_identifier: String, + unlocking_duration: Option, + lock_position_identifier: Option, funds: Vec, result: impl Fn(Result), ) -> &mut Self { @@ -220,6 +325,8 @@ impl TestingSuite { pair_identifier, slippage_tolerance: None, receiver: None, + unlocking_duration, + lock_position_identifier, }; result( @@ -616,4 +723,24 @@ impl TestingSuite { self } + + #[track_caller] + pub(crate) fn query_incentive_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 + } } diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index ce3f4cad..9f3d5296 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -14,7 +14,7 @@ pub struct InstantiateMsg { /// 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, + pub bonding_manager_addr: String, /// The fee that must be paid to create an incentive. pub create_incentive_fee: Coin, /// The maximum amount of incentives that can exist for a single LP token at a time. diff --git a/packages/white-whale-std/src/pool_manager.rs b/packages/white-whale-std/src/pool_manager.rs index 8b98b6a0..9e715379 100644 --- a/packages/white-whale-std/src/pool_manager.rs +++ b/packages/white-whale-std/src/pool_manager.rs @@ -111,7 +111,10 @@ impl PairInfo {} #[cw_serde] pub struct Config { - pub whale_lair_addr: Addr, + /// The address of the bonding manager contract. + pub bonding_manager_addr: Addr, + /// The address of the incentive manager contract. + pub incentive_manager_addr: Addr, // We must set a creation fee on instantiation to prevent spamming of pools pub pool_creation_fee: Coin, // Whether or not swaps, deposits, and withdrawals are enabled @@ -120,7 +123,8 @@ pub struct Config { #[cw_serde] pub struct InstantiateMsg { - pub fee_collector_addr: String, + pub bonding_manager_addr: String, + pub incentive_manager_addr: String, pub pool_creation_fee: Coin, } @@ -143,6 +147,11 @@ pub enum ExecuteMsg { slippage_tolerance: Option, receiver: Option, pair_identifier: String, + /// The amount of time in seconds to unlock tokens if taking part on the incentives. If not passed, + /// the tokens will not be locked and the LP tokens will be returned to the user. + unlocking_duration: Option, + /// The identifier of the position to lock the LP tokens in the incentive manager, if any. + lock_position_identifier: Option, }, /// Swap an offer asset to the other Swap {