diff --git a/Cargo.lock b/Cargo.lock index b82a0ff8..0bdb6a3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -940,6 +940,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 90e62afc..ffbc1c1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ incentive-factory = { path = "./contracts/liquidity_hub/pool-network/incentive_f 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..15e5d3d2 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -213,7 +213,7 @@ impl TestingSuite { let msg = InstantiateMsg { owner: self.creator().to_string(), epoch_manager_addr, - whale_lair_addr, + whale_lair_addr: bonding_manager_addr, create_incentive_fee, max_concurrent_incentives, max_incentive_epoch_buffer, @@ -257,7 +257,7 @@ impl TestingSuite { let msg = InstantiateMsg { owner: self.creator().to_string(), epoch_manager_addr, - whale_lair_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/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index 44ec8b86..824da61e 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -25,7 +25,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 { @@ -70,6 +71,8 @@ pub fn execute( slippage_tolerance, receiver, pair_identifier, + unlocking_duration, + lock_position_identifier, } => liquidity::commands::provide_liquidity( deps, env, @@ -77,6 +80,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 c0139221..16a12f57 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -108,7 +108,7 @@ pub fn create_pair( // send pair creation fee to whale lair i.e the new fee_collector messages.push(fill_rewards_msg( - 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 26771f76..0098df64 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -116,7 +116,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::FillRewards { assets: vec![swap_result.protocol_fee_asset.clone()], })?, @@ -127,7 +127,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 66ae981b..7cfbe6bb 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -64,7 +64,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::FillRewards { assets: vec![swap_result.protocol_fee_asset.clone()], })?, @@ -75,7 +75,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 4adfa37b..8e8ba6a6 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() { @@ -319,6 +323,7 @@ mod router { use cosmwasm_std::Event; use super::*; + #[test] fn basic_swap_operations_test() { let mut suite = TestingSuite::default_with_balances(vec![ @@ -396,6 +401,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -417,6 +424,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -481,21 +490,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); @@ -583,6 +592,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -604,6 +615,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -720,6 +733,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -742,6 +757,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -787,7 +804,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(), }) ); }, @@ -875,6 +892,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -896,6 +915,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1095,6 +1116,8 @@ mod router { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1116,6 +1139,8 @@ mod router { suite.provide_liquidity( creator.clone(), "uluna-uusd".to_string(), + None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1171,7 +1196,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), }) ) }, @@ -1256,6 +1281,8 @@ mod swapping { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1468,6 +1495,8 @@ mod swapping { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1670,6 +1699,8 @@ mod swapping { suite.provide_liquidity( creator.clone(), "whale-uluna".to_string(), + None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1746,7 +1777,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)); @@ -1879,8 +1910,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 e0c22a32..b538f637 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -1,22 +1,23 @@ use cosmwasm_std::testing::MockStorage; -use white_whale_std::pool_manager::{ - Config, FeatureToggle, PairInfoResponse, SwapOperation, SwapRouteResponse, SwapRoutesResponse, -}; -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_manager::{ + Config, FeatureToggle, PairInfoResponse, SwapOperation, SwapRouteResponse, SwapRoutesResponse, +}; +use white_whale_std::pool_manager::{InstantiateMsg, PairInfo}; 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, @@ -28,7 +29,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, @@ -39,6 +40,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, @@ -55,8 +80,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, } @@ -112,16 +139,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"), }; @@ -146,19 +180,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(), @@ -174,14 +214,76 @@ impl TestingSuite { let creator = self.creator().clone(); - self.whale_lair_addr = self + self.bonding_manager_addr = self + .app + .instantiate_contract( + bonding_manager_id, + creator.clone(), + &msg, + &[], + "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( - whale_lair_id, + incentive_manager_id, creator.clone(), &msg, &[], - "White Whale Lair".to_string(), + "Incentive Manager".to_string(), Some(creator.to_string()), ) .unwrap(); @@ -212,6 +314,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 { @@ -219,6 +323,8 @@ impl TestingSuite { pair_identifier, slippage_tolerance: None, receiver: None, + unlocking_duration, + lock_position_identifier, }; result( @@ -563,4 +669,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 9ddd957c..5079573e 100644 --- a/packages/white-whale-std/src/pool_manager.rs +++ b/packages/white-whale-std/src/pool_manager.rs @@ -105,7 +105,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 @@ -114,7 +117,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, } @@ -137,6 +141,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 {