From 6c753c41e72ef5ea60bfa3363657bbc6e775f72d Mon Sep 17 00:00:00 2001 From: Donovan Solms Date: Wed, 13 Sep 2023 13:19:08 +0200 Subject: [PATCH] feat(fee_share): implement swap fee sharing on all pool types (#90) * feat(fee_share): Implement fee sharing in swap * feat(fee_share): Implement fee sharing for stableswap * feat(fee_share): Emit shared amount in swap response * feat(fee_share): Implement fee sharing for PCL * fix(fee_sharing): Bump PCL version * fix(fee_share): Output full correct amount * fix(fee_share): Bump versions of pairs * fix(fee_share): Bump migration version * fix(fee_share): Bump cosmwasm-check to 1.4.0 --- .github/workflows/check_artifacts.yml | 2 +- Cargo.lock | 8 +- contracts/pair/Cargo.toml | 2 +- contracts/pair/src/contract.rs | 80 ++- contracts/pair/src/error.rs | 8 +- contracts/pair/src/migration.rs | 1 + contracts/pair/src/state.rs | 7 +- contracts/pair/src/testing.rs | 3 + contracts/pair/tests/integration.rs | 540 +++++++++++++++++- contracts/pair_concentrated/Cargo.toml | 6 +- contracts/pair_concentrated/src/contract.rs | 68 ++- contracts/pair_concentrated/src/error.rs | 8 +- contracts/pair_concentrated/src/migration.rs | 1 + contracts/pair_concentrated/src/queries.rs | 7 + contracts/pair_concentrated/src/state.rs | 3 + contracts/pair_concentrated/src/utils.rs | 19 +- contracts/pair_concentrated/tests/helper.rs | 4 +- .../tests/pair_concentrated_integration.rs | 107 +++- .../tests/pair_concentrated_simulation.rs | 75 ++- .../pair_concentrated_inj/src/migrate.rs | 2 +- contracts/pair_stable/Cargo.toml | 2 +- contracts/pair_stable/src/contract.rs | 84 ++- contracts/pair_stable/src/error.rs | 8 +- contracts/pair_stable/src/migration.rs | 2 + contracts/pair_stable/src/state.rs | 3 + contracts/pair_stable/src/testing.rs | 2 + contracts/pair_stable/tests/integration.rs | 495 +++++++++++++++- .../periphery/liquidity_manager/src/utils.rs | 1 + packages/astroport/Cargo.toml | 4 +- packages/astroport/src/liquidity_manager.rs | 4 +- packages/astroport/src/pair.rs | 37 +- packages/astroport/src/pair_concentrated.rs | 14 +- 32 files changed, 1554 insertions(+), 53 deletions(-) diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml index 8231309fa..9b90e55b8 100644 --- a/.github/workflows/check_artifacts.yml +++ b/.github/workflows/check_artifacts.yml @@ -112,7 +112,7 @@ jobs: fail-on-cache-miss: true - name: Install cosmwasm-check # Uses --debug for compilation speed - run: cargo install --debug --version 1.3.1 cosmwasm-check + run: cargo install --debug --version 1.4.0 cosmwasm-check - name: Cosmwasm check run: | cosmwasm-check $GITHUB_WORKSPACE/artifacts/*.wasm --available-capabilities staking,cosmwasm_1_1,injective,iterator,stargate diff --git a/Cargo.lock b/Cargo.lock index 0b1752968..cab228f0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ dependencies = [ [[package]] name = "astroport" -version = "3.5.0" +version = "3.6.0" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -373,7 +373,7 @@ dependencies = [ [[package]] name = "astroport-pair" -version = "1.4.0" +version = "1.5.0" dependencies = [ "astroport", "astroport-factory", @@ -439,7 +439,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "astroport", @@ -491,7 +491,7 @@ dependencies = [ [[package]] name = "astroport-pair-stable" -version = "3.2.0" +version = "3.3.0" dependencies = [ "anyhow", "astroport", diff --git a/contracts/pair/Cargo.toml b/contracts/pair/Cargo.toml index d817b3ce8..aa93ce3a1 100644 --- a/contracts/pair/Cargo.toml +++ b/contracts/pair/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair" -version = "1.4.0" +version = "1.5.0" authors = ["Astroport"] edition = "2021" description = "The Astroport constant product pool contract implementation" diff --git a/contracts/pair/src/contract.rs b/contracts/pair/src/contract.rs index e0270612f..8bf979bbd 100644 --- a/contracts/pair/src/contract.rs +++ b/contracts/pair/src/contract.rs @@ -19,8 +19,8 @@ use astroport::asset::{ use astroport::factory::PairType; use astroport::generator::Cw20HookMsg as GeneratorHookMsg; use astroport::pair::{ - ConfigResponse, XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, DEFAULT_SLIPPAGE, - MAX_ALLOWED_SLIPPAGE, + ConfigResponse, FeeShareConfig, XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, + DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, MAX_FEE_SHARE_BPS, }; use astroport::pair::{ CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, PoolResponse, @@ -80,6 +80,7 @@ pub fn instantiate( price0_cumulative_last: Uint128::zero(), price1_cumulative_last: Uint128::zero(), track_asset_balances, + fee_share: None, }; if track_asset_balances { @@ -691,12 +692,39 @@ pub fn swap( messages.push(return_asset.into_msg(receiver.clone())?) } + // If this pool is configured to share fees, calculate the amount to send + // to the receiver and add the transfer message + // The calculation works as follows: We take the share percentage first, + // and the remainder is then split between LPs and maker + let mut fees_commission_amount = commission_amount; + let mut fee_share_amount = Uint128::zero(); + if let Some(fee_share) = config.fee_share.clone() { + // Calculate the fee share amount from the full commission amount + let share_fee_rate = Decimal::from_ratio(fee_share.bps, 10000u16); + fee_share_amount = fees_commission_amount * share_fee_rate; + + if !fee_share_amount.is_zero() { + // Subtract the fee share amount from the commission + fees_commission_amount = fees_commission_amount.saturating_sub(fee_share_amount); + + // Build send message for the shared amount + let fee_share_msg = Asset { + info: ask_pool.info.clone(), + amount: fee_share_amount, + } + .into_msg(fee_share.recipient)?; + messages.push(fee_share_msg); + } + } + // Compute the Maker fee let mut maker_fee_amount = Uint128::zero(); if let Some(fee_address) = fee_info.fee_address { - if let Some(f) = - calculate_maker_fee(&ask_pool.info, commission_amount, fee_info.maker_fee_rate) - { + if let Some(f) = calculate_maker_fee( + &ask_pool.info, + fees_commission_amount, + fee_info.maker_fee_rate, + ) { maker_fee_amount = f.amount; messages.push(f.into_msg(fee_address)?); } @@ -712,7 +740,7 @@ pub fn swap( BALANCES.save( deps.storage, &ask_pool.info, - &(ask_pool.amount - return_amount - maker_fee_amount), + &(ask_pool.amount - return_amount - maker_fee_amount - fee_share_amount), env.block.height, )?; } @@ -744,6 +772,7 @@ pub fn swap( attr("spread_amount", spread_amount), attr("commission_amount", commission_amount), attr("maker_fee_amount", maker_fee_amount), + attr("fee_share_amount", fee_share_amount), ])) } @@ -787,6 +816,44 @@ pub fn update_config( "enabled".to_owned(), )); } + XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address, + } => { + // Enable fee sharing for this contract + // If fee sharing is already enabled, we should be able to overwrite + // the values currently set + + // Ensure the fee share isn't 0 and doesn't exceed the maximum allowed value + if fee_share_bps == 0 || fee_share_bps > MAX_FEE_SHARE_BPS { + return Err(ContractError::FeeShareOutOfBounds {}); + } + + // Set sharing config + config.fee_share = Some(FeeShareConfig { + bps: fee_share_bps, + recipient: deps.api.addr_validate(&fee_share_address)?, + }); + + CONFIG.save(deps.storage, &config)?; + + response.attributes.push(attr("action", "enable_fee_share")); + response + .attributes + .push(attr("fee_share_bps", fee_share_bps.to_string())); + response + .attributes + .push(attr("fee_share_address", fee_share_address)); + } + XYKPoolUpdateParams::DisableFeeShare => { + // Disable fee sharing for this contract by setting bps and + // address back to None + config.fee_share = None; + CONFIG.save(deps.storage, &config)?; + response + .attributes + .push(attr("action", "disable_fee_share")); + } } Ok(response) @@ -1069,6 +1136,7 @@ pub fn query_config(deps: Deps) -> StdResult { block_time_last: config.block_time_last, params: Some(to_binary(&XYKPoolConfig { track_asset_balances: config.track_asset_balances, + fee_share: config.fee_share, })?), owner: factory_config.owner, factory_addr: config.factory_addr, diff --git a/contracts/pair/src/error.rs b/contracts/pair/src/error.rs index a24c42ba4..49346ba79 100644 --- a/contracts/pair/src/error.rs +++ b/contracts/pair/src/error.rs @@ -1,4 +1,4 @@ -use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; +use astroport::{asset::MINIMUM_LIQUIDITY_AMOUNT, pair::MAX_FEE_SHARE_BPS}; use cosmwasm_std::{OverflowError, StdError}; use thiserror::Error; @@ -52,6 +52,12 @@ pub enum ContractError { #[error("Failed to parse or process reply message")] FailedToParseReply {}, + + #[error( + "Fee share is 0 or exceeds maximum allowed value of {} bps", + MAX_FEE_SHARE_BPS + )] + FeeShareOutOfBounds {}, } impl From for ContractError { diff --git a/contracts/pair/src/migration.rs b/contracts/pair/src/migration.rs index e286450fd..a002ed834 100644 --- a/contracts/pair/src/migration.rs +++ b/contracts/pair/src/migration.rs @@ -38,6 +38,7 @@ pub(crate) fn add_asset_balances_tracking_flag( price0_cumulative_last: old_config.price0_cumulative_last, price1_cumulative_last: old_config.price1_cumulative_last, track_asset_balances: false, + fee_share: None, }; CONFIG.save(storage, &new_config)?; diff --git a/contracts/pair/src/state.rs b/contracts/pair/src/state.rs index d4008a858..74b6de0d0 100644 --- a/contracts/pair/src/state.rs +++ b/contracts/pair/src/state.rs @@ -1,4 +1,7 @@ -use astroport::asset::{AssetInfo, PairInfo}; +use astroport::{ + asset::{AssetInfo, PairInfo}, + pair::FeeShareConfig, +}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::{Item, SnapshotMap}; @@ -18,6 +21,8 @@ pub struct Config { pub price1_cumulative_last: Uint128, /// Whether asset balances are tracked over blocks or not. pub track_asset_balances: bool, + // The config for swap fee sharing + pub fee_share: Option, } /// Stores the config struct at the given key diff --git a/contracts/pair/src/testing.rs b/contracts/pair/src/testing.rs index 281cd7b9b..37ff5de9d 100644 --- a/contracts/pair/src/testing.rs +++ b/contracts/pair/src/testing.rs @@ -931,6 +931,7 @@ fn try_native_to_token() { attr("spread_amount", expected_spread_amount.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); @@ -1121,6 +1122,7 @@ fn try_token_to_native() { attr("spread_amount", expected_spread_amount.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); @@ -1418,6 +1420,7 @@ fn test_accumulate_prices() { price0_cumulative_last: Uint128::new(case.last0), price1_cumulative_last: Uint128::new(case.last1), track_asset_balances: false, + fee_share: None, }, Uint128::new(case.x_amount), Uint128::new(case.y_amount), diff --git a/contracts/pair/tests/integration.rs b/contracts/pair/tests/integration.rs index 61980944d..a7851199b 100644 --- a/contracts/pair/tests/integration.rs +++ b/contracts/pair/tests/integration.rs @@ -9,14 +9,15 @@ use astroport::factory::{ QueryMsg as FactoryQueryMsg, }; use astroport::pair::{ - ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, - XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, TWAP_PRECISION, + ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, FeeShareConfig, + InstantiateMsg, PoolResponse, QueryMsg, XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, + MAX_FEE_SHARE_BPS, TWAP_PRECISION, }; use astroport::token::InstantiateMsg as TokenInstantiateMsg; use astroport_mocks::cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; use astroport_mocks::{astroport_address, MockGeneratorBuilder, MockXykPairBuilder}; use astroport_pair::error::ContractError; -use cosmwasm_std::{attr, to_binary, Addr, Coin, Decimal, Uint128}; +use cosmwasm_std::{attr, to_binary, Addr, Coin, Decimal, Uint128, Uint64}; use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; const OWNER: &str = "owner"; @@ -339,7 +340,8 @@ fn test_provide_and_withdraw_liquidity() { block_time_last: router.block_info().time.seconds(), params: Some( to_binary(&XYKPoolConfig { - track_asset_balances: false + track_asset_balances: false, + fee_share: None, }) .unwrap() ), @@ -1453,7 +1455,8 @@ fn update_pair_config() { block_time_last: 0, params: Some( to_binary(&XYKPoolConfig { - track_asset_balances: false + track_asset_balances: false, + fee_share: None, }) .unwrap() ), @@ -1493,7 +1496,199 @@ fn update_pair_config() { block_time_last: 0, params: Some( to_binary(&XYKPoolConfig { - track_asset_balances: true + track_asset_balances: true, + fee_share: None, + }) + .unwrap() + ), + owner: Addr::unchecked("owner"), + factory_addr: Addr::unchecked("contract0") + } + ); +} + +#[test] +fn enable_disable_fee_sharing() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let factory_code_id = store_factory_code(&mut router); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![], + token_code_id: token_contract_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_instance.to_string(), + init_params: None, + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + assert_eq!( + res, + ConfigResponse { + block_time_last: 0, + params: Some( + to_binary(&XYKPoolConfig { + track_asset_balances: false, + fee_share: None, + }) + .unwrap() + ), + owner: Addr::unchecked("owner"), + factory_addr: Addr::unchecked("contract0") + } + ); + + // Attemt to set fee sharing higher than maximum + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps: MAX_FEE_SHARE_BPS + 1, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + // Attemt to set fee sharing to 0 + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps: 0, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + let fee_share_bps = 500; // 5% + let fee_share_contract = "contract".to_string(); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_contract.clone(), + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + res, + ConfigResponse { + block_time_last: 0, + params: Some( + to_binary(&XYKPoolConfig { + track_asset_balances: false, + fee_share: Some(FeeShareConfig { + bps: fee_share_bps, + recipient: Addr::unchecked(fee_share_contract), + }), + }) + .unwrap() + ), + owner: Addr::unchecked("owner"), + factory_addr: Addr::unchecked("contract0") + } + ); + + // Disable fee sharing + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::DisableFeeShare).unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + res, + ConfigResponse { + block_time_last: 0, + params: Some( + to_binary(&XYKPoolConfig { + track_asset_balances: false, + fee_share: None, }) .unwrap() ), @@ -1651,3 +1846,336 @@ fn test_imbalanced_withdraw_is_disabled() { "Generic error: Imbalanced withdraw is currently disabled" ); } + +#[test] +fn check_correct_fee_share() { + // Validate the resulting values + // We swapped 1_000000 of token X + // Fee is set to 0.3% of the swap amount resulting in 1000000 * 0.003 = 3000 + // User receives with 1000000 - 3000 = 997000 + // Of the 3000 fee, 10% is sent to the fee sharing contract resulting in 300 + // Of the 2700 fee left, 33.33% is sent to the maker resulting in 899 + // Of the 1801 fee left, all of it is left in the pool + + // Test with 10% fee share, 0.3% total fee and 33.33% maker fee + test_fee_share( + 3333u16, + 30u16, + 1000u16, + Uint128::from(300u64), + Uint128::from(899u64), + ); + + // Test with 5% fee share, 0.3% total fee and 50% maker fee + test_fee_share( + 5000u16, + 30u16, + 500u16, + Uint128::from(150u64), + Uint128::from(1425u64), + ); + + // Test with 5% fee share, 0.1% total fee and 33.33% maker fee + test_fee_share( + 3333u16, + 10u16, + 500u16, + Uint128::from(50u64), + Uint128::from(316u64), + ); +} + +fn test_fee_share( + maker_fee_bps: u16, + total_fee_bps: u16, + fee_share_bps: u16, + expected_fee_share: Uint128, + expected_maker_fee: Uint128, +) { + let owner = Addr::unchecked(OWNER); + + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(1_000_000_000000); + let y_amount = Uint128::new(1_000_000_000000); + let x_offer = Uint128::new(1_000000); + let maker_address = "maker"; + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let init_msg = FactoryInstantiateMsg { + fee_address: Some(maker_address.to_string()), + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps, + pair_type: PairType::Xyk {}, + total_fee_bps, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + pair_type: PairType::Xyk {}, + init_params: Some( + to_binary(&XYKPoolParams { + track_asset_balances: Some(true), + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let user = Addr::unchecked("user"); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let fee_share_address = "contract_receiver".to_string(); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let swap_msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + // try to swap after provide liquidity + app.execute_contract(owner.clone(), token_x_instance.clone(), &swap_msg, &[]) + .unwrap(); + + let y_expected_return = + x_offer - Uint128::from((x_offer * Decimal::from_ratio(total_fee_bps, 10000u64)).u128()); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, y_expected_return); + + let msg = Cw20QueryMsg::Balance { + address: fee_share_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + let acceptable_spread_amount = Uint128::new(1); + assert_eq!(res.balance, expected_fee_share - acceptable_spread_amount); + + let msg = Cw20QueryMsg::Balance { + address: maker_address.to_string(), + }; + // Assert maker fee is correct + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_maker_fee); + + app.update_block(|b| b.height += 1); + + // Assert LP balances are correct + let msg = QueryMsg::Pool {}; + let res: PoolResponse = app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + let acceptable_spread_amount = Uint128::new(1); + assert_eq!(res.assets[0].amount, x_amount + x_offer); + assert_eq!( + res.assets[1].amount, + y_amount - y_expected_return - expected_maker_fee - expected_fee_share + + acceptable_spread_amount + ); + + // Assert LP balances tracked are correct + let msg = QueryMsg::AssetBalanceAt { + asset_info: AssetInfo::Token { + contract_addr: token_y_instance, + }, + block_height: Uint64::from(app.block_info().height), + }; + let res: Option = app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + assert_eq!( + res.unwrap(), + y_amount - y_expected_return - expected_maker_fee - expected_fee_share + + acceptable_spread_amount + ); +} diff --git a/contracts/pair_concentrated/Cargo.toml b/contracts/pair_concentrated/Cargo.toml index a8a65ff00..e9b2293cc 100644 --- a/contracts/pair_concentrated/Cargo.toml +++ b/contracts/pair_concentrated/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated" -version = "2.1.0" +version = "2.2.0" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair" @@ -25,7 +25,9 @@ library = [] [dependencies] astroport = { path = "../../packages/astroport", version = "3" } -astroport-factory = { path = "../factory", features = ["library"], version = "1" } +astroport-factory = { path = "../factory", features = [ + "library", +], version = "1" } astroport-circular-buffer = { path = "../../packages/circular_buffer", version = "0.1" } cw2 = "0.15" cw20 = "0.15" diff --git a/contracts/pair_concentrated/src/contract.rs b/contracts/pair_concentrated/src/contract.rs index b275f79ab..67e8e5bd8 100644 --- a/contracts/pair_concentrated/src/contract.rs +++ b/contracts/pair_concentrated/src/contract.rs @@ -3,7 +3,7 @@ use std::vec; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, from_binary, wasm_execute, wasm_instantiate, Addr, Binary, CosmosMsg, Decimal, + attr, from_binary, wasm_execute, wasm_instantiate, Addr, Attribute, Binary, CosmosMsg, Decimal, Decimal256, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, SubMsg, SubMsgResponse, SubMsgResult, Uint128, }; @@ -21,7 +21,7 @@ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_ow use astroport::cosmwasm_ext::{AbsDiff, DecimalToInteger, IntegerToDecimal}; use astroport::factory::PairType; use astroport::observation::{PrecommitObservation, MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; -use astroport::pair::{Cw20HookMsg, ExecuteMsg, InstantiateMsg}; +use astroport::pair::{Cw20HookMsg, ExecuteMsg, FeeShareConfig, InstantiateMsg, MAX_FEE_SHARE_BPS}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, MigrateMsg, UpdatePoolParams, }; @@ -118,6 +118,7 @@ pub fn instantiate( pool_state, owner: None, track_asset_balances: params.track_asset_balances.unwrap_or_default(), + fee_share: None, }; if config.track_asset_balances { @@ -735,6 +736,11 @@ fn swap( if fee_info.fee_address.is_some() { maker_fee_share = fee_info.maker_fee_rate.into(); } + // If this pool is configured to share fees + let mut share_fee_share = Decimal256::zero(); + if let Some(fee_share) = config.fee_share.clone() { + share_fee_share = Decimal256::from_ratio(fee_share.bps, 10000u16); + } let swap_result = compute_swap( &xs, @@ -743,9 +749,10 @@ fn swap( &config, &env, maker_fee_share, + share_fee_share, )?; xs[offer_ind] += offer_asset_dec.amount; - xs[ask_ind] -= swap_result.dy + swap_result.maker_fee; + xs[ask_ind] -= swap_result.dy + swap_result.maker_fee + swap_result.share_fee; let return_amount = swap_result.dy.to_uint(ask_asset_prec)?; let spread_amount = swap_result.spread_fee.to_uint(ask_asset_prec)?; @@ -776,6 +783,17 @@ fn swap( } .into_msg(&receiver)?]; + // Send the shared fee + let mut fee_share_amount = Uint128::zero(); + if let Some(fee_share) = config.fee_share.clone() { + fee_share_amount = swap_result.share_fee.to_uint(ask_asset_prec)?; + if !fee_share_amount.is_zero() { + let fee = pools[ask_ind].info.with_balance(fee_share_amount); + messages.push(fee.into_msg(fee_share.recipient)?); + } + } + + // Send the maker fee let mut maker_fee = Uint128::zero(); if let Some(fee_address) = fee_info.fee_address { maker_fee = swap_result.maker_fee.to_uint(ask_asset_prec)?; @@ -812,7 +830,10 @@ fn swap( BALANCES.save( deps.storage, &pools[ask_ind].info, - &(pools[ask_ind].amount.to_uint(ask_asset_prec)? - return_amount - maker_fee), + &(pools[ask_ind].amount.to_uint(ask_asset_prec)? + - return_amount + - maker_fee + - fee_share_amount), env.block.height, )?; } @@ -831,6 +852,7 @@ fn swap( swap_result.total_fee.to_uint(ask_asset_prec)?, ), attr("maker_fee_amount", maker_fee), + attr("fee_share_amount", fee_share_amount), ])) } @@ -851,6 +873,8 @@ fn update_config( return Err(ContractError::Unauthorized {}); } + let mut attrs: Vec = vec![]; + let action = match from_binary::(¶ms)? { ConcentratedPoolUpdateParams::Update(update_params) => { config.pool_params.update_params(update_params)?; @@ -880,10 +904,44 @@ fn update_config( "enable_asset_balances_tracking" } + ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address, + } => { + // Enable fee sharing for this contract + // If fee sharing is already enabled, we should be able to overwrite + // the values currently set + + // Ensure the fee share isn't 0 and doesn't exceed the maximum allowed value + if fee_share_bps == 0 || fee_share_bps > MAX_FEE_SHARE_BPS { + return Err(ContractError::FeeShareOutOfBounds {}); + } + + // Set sharing config + config.fee_share = Some(FeeShareConfig { + bps: fee_share_bps, + recipient: deps.api.addr_validate(&fee_share_address)?, + }); + + CONFIG.save(deps.storage, &config)?; + + attrs.push(attr("fee_share_bps", fee_share_bps.to_string())); + attrs.push(attr("fee_share_address", fee_share_address)); + "enable_fee_share" + } + ConcentratedPoolUpdateParams::DisableFeeShare => { + // Disable fee sharing for this contract by setting bps and + // address back to None + config.fee_share = None; + CONFIG.save(deps.storage, &config)?; + "disable_fee_share" + } }; CONFIG.save(deps.storage, &config)?; - Ok(Response::new().add_attribute("action", action)) + Ok(Response::new() + .add_attribute("action", action) + .add_attributes(attrs)) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/pair_concentrated/src/error.rs b/contracts/pair_concentrated/src/error.rs index 6c06b2cf6..af9378bc7 100644 --- a/contracts/pair_concentrated/src/error.rs +++ b/contracts/pair_concentrated/src/error.rs @@ -1,5 +1,5 @@ use crate::consts::MIN_AMP_CHANGING_TIME; -use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; +use astroport::{asset::MINIMUM_LIQUIDITY_AMOUNT, pair::MAX_FEE_SHARE_BPS}; use astroport_circular_buffer::error::BufferError; use cosmwasm_std::{ConversionOverflowError, Decimal, OverflowError, StdError}; use thiserror::Error; @@ -77,4 +77,10 @@ pub enum ContractError { #[error("Asset balances tracking is already enabled")] AssetBalancesTrackingIsAlreadyEnabled {}, + + #[error( + "Fee share is 0 or exceeds maximum allowed value of {} bps", + MAX_FEE_SHARE_BPS + )] + FeeShareOutOfBounds {}, } diff --git a/contracts/pair_concentrated/src/migration.rs b/contracts/pair_concentrated/src/migration.rs index 138404ddb..9ae1788c8 100644 --- a/contracts/pair_concentrated/src/migration.rs +++ b/contracts/pair_concentrated/src/migration.rs @@ -72,6 +72,7 @@ pub(crate) fn migrate_config(storage: &mut dyn Storage) -> Result<(), StdError> pool_state: old_config.pool_state, owner: old_config.owner, track_asset_balances: false, + fee_share: None, }; CONFIG.save(storage, &new_config)?; diff --git a/contracts/pair_concentrated/src/queries.rs b/contracts/pair_concentrated/src/queries.rs index ab4f96d1c..bd9499150 100644 --- a/contracts/pair_concentrated/src/queries.rs +++ b/contracts/pair_concentrated/src/queries.rs @@ -161,6 +161,11 @@ pub fn query_simulation( if fee_info.fee_address.is_some() { maker_fee_share = fee_info.maker_fee_rate.into(); } + // If this pool is configured to share fees + let mut share_fee_share = Decimal256::zero(); + if let Some(fee_share) = config.fee_share.clone() { + share_fee_share = Decimal256::from_ratio(fee_share.bps, 10000u16); + } let swap_result = compute_swap( &xs, @@ -169,6 +174,7 @@ pub fn query_simulation( &config, &env, maker_fee_share, + share_fee_share, )?; Ok(SimulationResponse { @@ -258,6 +264,7 @@ pub fn query_config(deps: Deps, env: Env) -> StdResult { price_scale, ma_half_time: config.pool_params.ma_half_time, track_asset_balances: config.track_asset_balances, + fee_share: config.fee_share, })?), owner: config.owner.unwrap_or(factory_config.owner), factory_addr: config.factory_addr, diff --git a/contracts/pair_concentrated/src/state.rs b/contracts/pair_concentrated/src/state.rs index e9d520915..cbefdf502 100644 --- a/contracts/pair_concentrated/src/state.rs +++ b/contracts/pair_concentrated/src/state.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use astroport::pair::FeeShareConfig; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ Addr, Decimal, Decimal256, DepsMut, Env, Order, StdError, StdResult, Storage, Uint128, @@ -38,6 +39,8 @@ pub struct Config { pub owner: Option, /// Whether asset balances are tracked over blocks or not. pub track_asset_balances: bool, + // The config for swap fee sharing + pub fee_share: Option, } /// This structure stores the pool parameters which may be adjusted via the `update_pool_params`. diff --git a/contracts/pair_concentrated/src/utils.rs b/contracts/pair_concentrated/src/utils.rs index 47a508db7..061090272 100644 --- a/contracts/pair_concentrated/src/utils.rs +++ b/contracts/pair_concentrated/src/utils.rs @@ -238,6 +238,7 @@ pub struct SwapResult { pub dy: Decimal256, pub spread_fee: Decimal256, pub maker_fee: Decimal256, + pub share_fee: Decimal256, pub total_fee: Decimal256, } @@ -260,8 +261,16 @@ pub fn calc_last_prices(xs: &[Decimal256], config: &Config, env: &Env) -> StdRes offer_amount = Decimal256::raw(1u128); } - let last_price = compute_swap(xs, offer_amount, 1, config, env, Decimal256::zero())? - .calc_last_prices(offer_amount, 0); + let last_price = compute_swap( + xs, + offer_amount, + 1, + config, + env, + Decimal256::zero(), + Decimal256::zero(), + )? + .calc_last_prices(offer_amount, 0); Ok(last_price) } @@ -274,6 +283,7 @@ pub fn compute_swap( config: &Config, env: &Env, maker_fee_share: Decimal256, + share_fee_share: Decimal256, ) -> StdResult { let offer_ind = 1 ^ ask_ind; @@ -309,10 +319,13 @@ pub fn compute_swap( let total_fee = fee_rate * dy; dy -= total_fee; + let share_fee = total_fee * share_fee_share; + Ok(SwapResult { dy, spread_fee, - maker_fee: total_fee * maker_fee_share, + maker_fee: (total_fee - share_fee) * maker_fee_share, + share_fee, total_fee, }) } diff --git a/contracts/pair_concentrated/tests/helper.rs b/contracts/pair_concentrated/tests/helper.rs index dd11184ba..c7b617119 100644 --- a/contracts/pair_concentrated/tests/helper.rs +++ b/contracts/pair_concentrated/tests/helper.rs @@ -24,7 +24,7 @@ use astroport::pair::{ SimulationResponse, }; use astroport::pair_concentrated::{ - ConcentratedPoolParams, ConcentratedPoolUpdateParams, QueryMsg, + ConcentratedPoolConfig, ConcentratedPoolParams, ConcentratedPoolUpdateParams, QueryMsg, }; use astroport_mocks::cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; use astroport_pair_concentrated::contract::{execute, instantiate, reply}; @@ -508,7 +508,7 @@ impl Helper { .app .wrap() .query_wasm_smart(&self.pair_addr, &QueryMsg::Config {})?; - let params: ConcentratedPoolParams = from_slice( + let params: ConcentratedPoolConfig = from_slice( &config_resp .params .ok_or_else(|| StdError::generic_err("Params not found in config response!"))?, diff --git a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs index efc9ff521..219720083 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs @@ -11,7 +11,7 @@ use astroport::asset::{ }; use astroport::cosmwasm_ext::AbsDiff; use astroport::observation::OracleObservation; -use astroport::pair::{ExecuteMsg, PoolResponse}; +use astroport::pair::{ExecuteMsg, PoolResponse, MAX_FEE_SHARE_BPS}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, PromoteParams, QueryMsg, UpdatePoolParams, }; @@ -1708,3 +1708,108 @@ fn test_frontrun_before_initial_provide() { assert_eq!(pools[0].amount.u128(), 320_624088); assert_eq!(pools[1].amount.u128(), 32_000000); } + +#[test] +fn check_correct_fee_share() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; + + let params = ConcentratedPoolParams { + amp: f64_to_dec(40f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale: Decimal::one(), + ma_half_time: 600, + track_asset_balances: None, + }; + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + let share_recipient = Addr::unchecked("share_recipient"); + // Attempt setting fee share with max+1 fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: MAX_FEE_SHARE_BPS + 1, + fee_share_address: share_recipient.to_string(), + }; + let err = helper.update_config(&owner, &action).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::FeeShareOutOfBounds {} + ); + + // Attempt setting fee share with max+1 fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: 0, + fee_share_address: share_recipient.to_string(), + }; + let err = helper.update_config(&owner, &action).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::FeeShareOutOfBounds {} + ); + + helper.app.next_block(1000); + + // Set to 5% fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: 1000, + fee_share_address: share_recipient.to_string(), + }; + helper.update_config(&owner, &action).unwrap(); + + let config = helper.query_config().unwrap(); + let fee_share = config.fee_share.unwrap(); + assert_eq!(fee_share.bps, 1000u16); + assert_eq!(fee_share.recipient, share_recipient.to_string()); + + helper.app.next_block(1000); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + helper.app.next_block(1000); + + let user = Addr::unchecked("user"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + helper.swap(&user, &offer_asset, None).unwrap(); + + // Check that the shared fees are sent + let expected_fee_share = 26081u128; + let recipient_balance = helper.coin_balance(&test_coins[1], &share_recipient); + assert_eq!(recipient_balance, expected_fee_share); + + let provider = Addr::unchecked("provider"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(1_000_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000_000000u128), + ]; + helper.give_me_money(&assets, &provider); + helper.provide_liquidity(&provider, &assets).unwrap(); + + let offer_asset = helper.assets[&test_coins[1]].with_balance(100_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + helper.swap(&user, &offer_asset, None).unwrap(); + + helper + .withdraw_liquidity(&provider, 999_999354, vec![]) + .unwrap(); + + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + helper.swap(&user, &offer_asset, None).unwrap(); + + // Disable fee share + let action = ConcentratedPoolUpdateParams::DisableFeeShare {}; + helper.update_config(&owner, &action).unwrap(); + + let config = helper.query_config().unwrap(); + assert!(config.fee_share.is_none()); +} diff --git a/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs b/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs index b0392269b..49215c331 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs @@ -7,7 +7,7 @@ mod helper; use crate::helper::{dec_to_f64, f64_to_dec, AppExtension, Helper, TestCoin}; use astroport::asset::AssetInfoExt; use astroport::cosmwasm_ext::AbsDiff; -use astroport::pair_concentrated::ConcentratedPoolParams; +use astroport::pair_concentrated::{ConcentratedPoolParams, ConcentratedPoolUpdateParams}; use astroport_pair_concentrated::error::ContractError; use cosmwasm_std::{Addr, Decimal, Decimal256}; use proptest::prelude::*; @@ -75,6 +75,71 @@ fn simulate_case(case: Vec<(usize, u128, u64)>) { } } +fn simulate_fee_share_case(case: Vec<(usize, u128, u64)>) { + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user"); + + let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; + + let params = ConcentratedPoolParams { + amp: f64_to_dec(40f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale: Decimal::one(), + ma_half_time: 600, + track_asset_balances: None, + }; + + let balances = vec![100_000_000_000000u128, 100_000_000_000000u128]; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Set to 5% fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: 1000, + fee_share_address: "share_address".to_string(), + }; + helper.update_config(&owner, &action).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(balances[0]), + helper.assets[&test_coins[1]].with_balance(balances[1]), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let mut i = 0; + for (offer_ind, dy, shift_time) in case { + let _ask_ind = 1 - offer_ind; + + println!("i: {i}, {offer_ind} {dy} {shift_time}"); + let offer_asset = helper.assets[&test_coins[offer_ind]].with_balance(dy); + // let balance_before = helper.coin_balance(&test_coins[ask_ind], &user); + helper.give_me_money(&[offer_asset.clone()], &user); + if let Err(err) = helper.swap(&user, &offer_asset, None) { + let err: ContractError = err.downcast().unwrap(); + match err { + ContractError::MaxSpreadAssertion {} => { + // if swap fails because of spread then skip this case + println!("exceeds spread limit"); + } + _ => panic!("{err}"), + } + + i += 1; + continue; + }; + // let swap_amount = helper.coin_balance(&test_coins[ask_ind], &user) - balance_before; + i += 1; + + // Shift time so EMA will update oracle prices + helper.app.next_block(shift_time); + } +} + fn simulate_provide_case(case: Vec<(impl Into, u128, u128, u64)>) { let owner = Addr::unchecked("owner"); let loss_tolerance = 0.05; // allowed loss per provide due to integer math withing contract @@ -592,6 +657,14 @@ proptest! { } } +proptest! { + #[ignore] + #[test] + fn simulate_fee_share_transactions(case in generate_cases()) { + simulate_fee_share_case(case); + } +} + proptest! { #[ignore] #[test] diff --git a/contracts/pair_concentrated_inj/src/migrate.rs b/contracts/pair_concentrated_inj/src/migrate.rs index 3d84cbaae..e09873988 100644 --- a/contracts/pair_concentrated_inj/src/migrate.rs +++ b/contracts/pair_concentrated_inj/src/migrate.rs @@ -13,7 +13,7 @@ use astroport_pair_concentrated::state::Config as CLConfig; use crate::state::{AmpGamma, Config, PoolParams, PoolState, PriceState, CONFIG}; const MIGRATE_FROM: &str = "astroport-pair-concentrated"; -const MIGRATION_VERSION: &str = "2.1.0"; +const MIGRATION_VERSION: &str = "2.2.0"; /// Manages the contract migration. #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/pair_stable/Cargo.toml b/contracts/pair_stable/Cargo.toml index 513646947..25a0af206 100644 --- a/contracts/pair_stable/Cargo.toml +++ b/contracts/pair_stable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-stable" -version = "3.2.0" +version = "3.3.0" authors = ["Astroport"] edition = "2021" description = "The Astroport stableswap pair contract implementation" diff --git a/contracts/pair_stable/src/contract.rs b/contracts/pair_stable/src/contract.rs index 3cec5ece0..d48323c9d 100644 --- a/contracts/pair_stable/src/contract.rs +++ b/contracts/pair_stable/src/contract.rs @@ -23,8 +23,8 @@ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_ow use astroport::cosmwasm_ext::IntegerToDecimal; use astroport::factory::PairType; use astroport::pair::{ - ConfigResponse, InstantiateMsg, StablePoolParams, StablePoolUpdateParams, DEFAULT_SLIPPAGE, - MAX_ALLOWED_SLIPPAGE, + ConfigResponse, FeeShareConfig, InstantiateMsg, StablePoolParams, StablePoolUpdateParams, + DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, MAX_FEE_SHARE_BPS, }; use crate::migration::{migrate_config_from_v21, migrate_config_to_v210}; @@ -106,6 +106,7 @@ pub fn instantiate( next_amp: params.amp * AMP_PRECISION, next_amp_time: env.block.time.seconds(), greatest_precision, + fee_share: None, }; CONFIG.save(deps.storage, &config)?; @@ -660,12 +661,39 @@ pub fn swap( messages.push(return_asset.into_msg(receiver.clone())?) } + // If this pool is configured to share fees, calculate the amount to send + // to the receiver and add the transfer message + // The calculation works as follows: We take the share percentage first, + // and the remainder is then split between LPs and maker + let mut fees_commission_amount = commission_amount; + let mut fee_share_amount = Uint128::zero(); + if let Some(fee_share) = config.fee_share { + // Calculate the fee share amount from the full commission amount + let share_fee_rate = Decimal::from_ratio(fee_share.bps, 10000u16); + fee_share_amount = fees_commission_amount * share_fee_rate; + + if !fee_share_amount.is_zero() { + // Subtract the fee share amount from the commission + fees_commission_amount = fees_commission_amount.saturating_sub(fee_share_amount); + + // Build send message for the shared amount + let fee_share_msg = Asset { + info: ask_pool.info.clone(), + amount: fee_share_amount, + } + .into_msg(fee_share.recipient)?; + messages.push(fee_share_msg); + } + } + // Compute the Maker fee let mut maker_fee_amount = Uint128::zero(); if let Some(fee_address) = fee_info.fee_address { - if let Some(f) = - calculate_maker_fee(&ask_pool.info, commission_amount, fee_info.maker_fee_rate) - { + if let Some(f) = calculate_maker_fee( + &ask_pool.info, + fees_commission_amount, + fee_info.maker_fee_rate, + ) { maker_fee_amount = f.amount; messages.push(f.into_msg(fee_address)?); } @@ -704,6 +732,7 @@ pub fn swap( attr("spread_amount", spread_amount), attr("commission_amount", commission_amount), attr("maker_fee_amount", maker_fee_amount), + attr("fee_share_amount", fee_share_amount), ])) } @@ -970,6 +999,7 @@ pub fn query_config(deps: Deps, env: Env) -> StdResult { block_time_last: config.block_time_last, params: Some(to_binary(&StablePoolConfig { amp: Decimal::from_ratio(compute_current_amp(&config, &env)?, AMP_PRECISION), + fee_share: config.fee_share, })?), owner: config.owner.unwrap_or(factory_config.owner), factory_addr: config.factory_addr, @@ -1071,7 +1101,7 @@ pub fn update_config( info: MessageInfo, params: Binary, ) -> Result { - let config = CONFIG.load(deps.storage)?; + let mut config = CONFIG.load(deps.storage)?; let factory_config = query_factory_config(&deps.querier, &config.factory_addr)?; if info.sender @@ -1084,15 +1114,55 @@ pub fn update_config( return Err(ContractError::Unauthorized {}); } + let mut response = Response::default(); + match from_binary::(¶ms)? { StablePoolUpdateParams::StartChangingAmp { next_amp, next_amp_time, } => start_changing_amp(config, deps, env, next_amp, next_amp_time)?, StablePoolUpdateParams::StopChangingAmp {} => stop_changing_amp(config, deps, env)?, + StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address, + } => { + // Enable fee sharing for this contract + // If fee sharing is already enabled, we should be able to overwrite + // the values currently set + + // Ensure the fee share isn't 0 and doesn't exceed the maximum allowed value + if fee_share_bps == 0 || fee_share_bps > MAX_FEE_SHARE_BPS { + return Err(ContractError::FeeShareOutOfBounds {}); + } + + // Set sharing config + config.fee_share = Some(FeeShareConfig { + bps: fee_share_bps, + recipient: deps.api.addr_validate(&fee_share_address)?, + }); + + CONFIG.save(deps.storage, &config)?; + + response.attributes.push(attr("action", "enable_fee_share")); + response + .attributes + .push(attr("fee_share_bps", fee_share_bps.to_string())); + response + .attributes + .push(attr("fee_share_address", fee_share_address)); + } + StablePoolUpdateParams::DisableFeeShare => { + // Disable fee sharing for this contract by setting bps and + // address back to None + config.fee_share = None; + CONFIG.save(deps.storage, &config)?; + response + .attributes + .push(attr("action", "disable_fee_share")); + } } - Ok(Response::default()) + Ok(response) } /// Start changing the AMP value. diff --git a/contracts/pair_stable/src/error.rs b/contracts/pair_stable/src/error.rs index 66109f573..ed0ae3941 100644 --- a/contracts/pair_stable/src/error.rs +++ b/contracts/pair_stable/src/error.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{CheckedMultiplyRatioError, ConversionOverflowError, OverflowError, StdError}; use thiserror::Error; -use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; +use astroport::{asset::MINIMUM_LIQUIDITY_AMOUNT, pair::MAX_FEE_SHARE_BPS}; use astroport_circular_buffer::error::BufferError; use crate::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; @@ -89,6 +89,12 @@ pub enum ContractError { #[error("Failed to parse or process reply message")] FailedToParseReply {}, + + #[error( + "Fee share is 0 or exceeds maximum allowed value of {} bps", + MAX_FEE_SHARE_BPS + )] + FeeShareOutOfBounds {}, } impl From for ContractError { diff --git a/contracts/pair_stable/src/migration.rs b/contracts/pair_stable/src/migration.rs index bae384963..096b917e0 100644 --- a/contracts/pair_stable/src/migration.rs +++ b/contracts/pair_stable/src/migration.rs @@ -74,6 +74,7 @@ pub fn migrate_config_to_v210(mut deps: DepsMut) -> StdResult { next_amp: cfg_v100.next_amp, next_amp_time: cfg_v100.next_amp_time, greatest_precision, + fee_share: None, }; CONFIG.save(deps.storage, &cfg)?; @@ -123,6 +124,7 @@ pub fn migrate_config_from_v21(deps: DepsMut) -> StdResult<()> { next_amp: cfg_v212.next_amp, next_amp_time: cfg_v212.next_amp_time, greatest_precision: cfg_v212.greatest_precision, + fee_share: None, }; CONFIG.save(deps.storage, &cfg)?; diff --git a/contracts/pair_stable/src/state.rs b/contracts/pair_stable/src/state.rs index f9c62beb5..76bd09a0c 100644 --- a/contracts/pair_stable/src/state.rs +++ b/contracts/pair_stable/src/state.rs @@ -1,6 +1,7 @@ use astroport::asset::{AssetInfo, PairInfo}; use astroport::common::OwnershipProposal; use astroport::observation::Observation; +use astroport::pair::FeeShareConfig; use astroport_circular_buffer::CircularBuffer; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, DepsMut, StdResult, Storage}; @@ -27,6 +28,8 @@ pub struct Config { pub next_amp_time: u64, /// The greatest precision of assets in the pool pub greatest_precision: u8, + // The config for swap fee sharing + pub fee_share: Option, } /// Circular buffer to store trade size observations diff --git a/contracts/pair_stable/src/testing.rs b/contracts/pair_stable/src/testing.rs index 3a967c060..a7c1cca03 100644 --- a/contracts/pair_stable/src/testing.rs +++ b/contracts/pair_stable/src/testing.rs @@ -775,6 +775,7 @@ fn try_native_to_token() { attr("spread_amount", 7593888.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); @@ -946,6 +947,7 @@ fn try_token_to_native() { attr("spread_amount", expected_spread_amount.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); diff --git a/contracts/pair_stable/tests/integration.rs b/contracts/pair_stable/tests/integration.rs index 6f92086a2..572bb5dea 100644 --- a/contracts/pair_stable/tests/integration.rs +++ b/contracts/pair_stable/tests/integration.rs @@ -6,9 +6,10 @@ use astroport::factory::{ QueryMsg as FactoryQueryMsg, }; use astroport::pair::{ - ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StablePoolConfig, - StablePoolParams, StablePoolUpdateParams, + ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, QueryMsg, + StablePoolConfig, StablePoolParams, StablePoolUpdateParams, MAX_FEE_SHARE_BPS, }; +use astroport_pair_stable::error::ContractError; use std::cell::RefCell; use std::rc::Rc; use std::str::FromStr; @@ -1264,6 +1265,176 @@ fn update_pair_config() { assert_eq!(params.amp, Decimal::from_ratio(150u32, 1u32)); } +#[test] +fn enable_disable_fee_sharing() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let coin_registry_address = instantiate_coin_registry( + &mut router, + Some(vec![("uusd".to_string(), 6), ("uluna".to_string(), 6)]), + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let factory_code_id = store_factory_code(&mut router); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![], + token_code_id: token_contract_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: coin_registry_address.to_string(), + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_instance.to_string(), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(100u32, 1u32)); + assert_eq!(params.fee_share, None); + + // Attemt to set fee sharing higher than maximum + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps: MAX_FEE_SHARE_BPS + 1, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + // Attemt to set fee sharing to 0 + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps: 0, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + let fee_share_bps = 500; // 5% + let fee_share_address = "contract".to_string(); + + // Set valid fee share + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + let set_fee_share = params.fee_share.unwrap(); + assert_eq!(set_fee_share.bps, fee_share_bps); + assert_eq!(set_fee_share.recipient, fee_share_address); + + // Disable fee share + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::DisableFeeShare {}).unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + assert!(params.fee_share.is_none()); +} + #[test] fn check_observe_queries() { let owner = Addr::unchecked("owner"); @@ -1509,3 +1680,323 @@ fn test_imbalance_withdraw_is_disabled() { "Generic error: Imbalanced withdraw is currently disabled" ); } + +#[test] +fn check_correct_fee_share() { + // Validate the resulting values + // We swapped 1_000000 of token X + // Fee is set to 0.05% of the swap amount resulting in 1000000 * 0.0005 = 500 + // User receives with 1000000 - 500 = 999500 + // Of the 500 fee, 10% is sent to the fee sharing contract resulting in 50 + + // Test with 10% fee share, 0.05% total fee and 50% maker fee + test_fee_share( + 5000u16, + 5u16, + 1000u16, + Uint128::from(50u64), + Uint128::from(225u64), + ); + + // Test with 5% fee share, 0.05% total fee and 50% maker fee + test_fee_share( + 5000u16, + 5u16, + 500u16, + Uint128::from(25u64), + Uint128::from(237u64), + ); + + // // Test with 5% fee share, 0.1% total fee and 33.33% maker fee + test_fee_share( + 3333u16, + 10u16, + 500u16, + Uint128::from(50u64), + Uint128::from(316u64), + ); +} + +fn test_fee_share( + maker_fee_bps: u16, + total_fee_bps: u16, + fee_share_bps: u16, + expected_fee_share: Uint128, + expected_maker_fee: Uint128, +) { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(1_000_000_000000); + let y_amount = Uint128::new(1_000_000_000000); + let x_offer = Uint128::new(1_000000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let maker_address = "maker".to_string(); + + let init_msg = FactoryInstantiateMsg { + fee_address: Some(maker_address.clone()), + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps, + total_fee_bps, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: String::from("owner0000"), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: Some(owner.to_string()), + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let d: u128 = app + .wrap() + .query_wasm_smart(&pair_instance, &QueryMsg::QueryComputeD {}) + .unwrap(); + assert_eq!(d, 2000000000000); + + // Set up 10% fee sharing + let fee_share_address = "contract_receiver".to_string(); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let user = Addr::unchecked("user"); + + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let y_expected_return = + x_offer - Uint128::from((x_offer * Decimal::from_ratio(total_fee_bps, 10000u64)).u128()); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, y_expected_return); + + let msg = Cw20QueryMsg::Balance { + address: fee_share_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_fee_share); + + let msg = Cw20QueryMsg::Balance { + address: maker_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_maker_fee); + + app.update_block(|b| b.height += 1); + + // Assert LP balances are correct + let msg = QueryMsg::Pool {}; + let res: PoolResponse = app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + assert_eq!(res.assets[0].amount, x_amount + x_offer); + assert_eq!( + res.assets[1].amount, + y_amount - y_expected_return - expected_maker_fee - expected_fee_share + ); +} diff --git a/contracts/periphery/liquidity_manager/src/utils.rs b/contracts/periphery/liquidity_manager/src/utils.rs index 78c8c814e..d68cef6b9 100644 --- a/contracts/periphery/liquidity_manager/src/utils.rs +++ b/contracts/periphery/liquidity_manager/src/utils.rs @@ -270,5 +270,6 @@ pub fn convert_config( next_amp: compat_config.next_amp, next_amp_time: compat_config.next_amp_time, greatest_precision, + fee_share: None, }) } diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml index ffc0d8538..e5d96eb37 100644 --- a/packages/astroport/Cargo.toml +++ b/packages/astroport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport" -version = "3.5.0" +version = "3.6.0" authors = ["Astroport"] edition = "2021" description = "Common Astroport types, queriers and other utils" @@ -29,4 +29,4 @@ cw3 = "1.0" # optional injective-math = { version = "0.1", optional = true } -thiserror = { version="1.0", optional = true } +thiserror = { version = "1.0", optional = true } diff --git a/packages/astroport/src/liquidity_manager.rs b/packages/astroport/src/liquidity_manager.rs index 9b218430a..e2f78f6bc 100644 --- a/packages/astroport/src/liquidity_manager.rs +++ b/packages/astroport/src/liquidity_manager.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{Addr, Uint128}; use cw20::Cw20ReceiveMsg; use crate::asset::{Asset, AssetInfo, PairInfo}; -use crate::pair::{Cw20HookMsg as PairCw20HookMsg, ExecuteMsg as PairExecuteMsg}; +use crate::pair::{Cw20HookMsg as PairCw20HookMsg, ExecuteMsg as PairExecuteMsg, FeeShareConfig}; #[cw_serde] pub struct InstantiateMsg { @@ -75,4 +75,6 @@ pub struct CompatPairStableConfig { pub price0_cumulative_last: Option, /// The last cumulative price 1 asset in pool pub price1_cumulative_last: Option, + // Fee sharing configuration + pub fee_share: Option, } diff --git a/packages/astroport/src/pair.rs b/packages/astroport/src/pair.rs index 174ed4590..6e6fab6fb 100644 --- a/packages/astroport/src/pair.rs +++ b/packages/astroport/src/pair.rs @@ -10,6 +10,8 @@ use cw20::Cw20ReceiveMsg; pub const DEFAULT_SLIPPAGE: &str = "0.005"; /// The maximum allowed swap slippage pub const MAX_ALLOWED_SLIPPAGE: &str = "0.5"; +/// The maximum fee share allowed, 10% +pub const MAX_FEE_SHARE_BPS: u16 = 1000; /// Decimal precision for TWAP results pub const TWAP_PRECISION: u8 = 6; @@ -151,6 +153,15 @@ pub struct ConfigResponse { pub factory_addr: Addr, } +/// Holds the configuration for fee sharing +#[cw_serde] +pub struct FeeShareConfig { + /// The fee shared with the address + pub bps: u16, + /// The share is sent to this address on every swap + pub recipient: Addr, +} + /// This structure holds the parameters that are returned from a swap simulation response #[cw_serde] pub struct SimulationResponse { @@ -203,6 +214,8 @@ pub struct XYKPoolParams { pub struct XYKPoolConfig { /// Whether asset balances are tracked over blocks or not. pub track_asset_balances: bool, + // The config for swap fee sharing + pub fee_share: Option, } /// This enum stores the option available to enable asset balances tracking over blocks. @@ -210,6 +223,14 @@ pub struct XYKPoolConfig { pub enum XYKPoolUpdateParams { /// Enables asset balances tracking over blocks. EnableAssetBalancesTracking, + /// Enables the sharing of swap fees with an external party. + EnableFeeShare { + /// The fee shared with the fee_share_address + fee_share_bps: u16, + /// The fee_share_bps is sent to this address on every swap + fee_share_address: String, + }, + DisableFeeShare, } /// This structure holds stableswap pool parameters. @@ -226,13 +247,26 @@ pub struct StablePoolParams { pub struct StablePoolConfig { /// The stableswap pool amplification pub amp: Decimal, + // The config for swap fee sharing + pub fee_share: Option, } /// This enum stores the options available to start and stop changing a stableswap pool's amplification. #[cw_serde] pub enum StablePoolUpdateParams { - StartChangingAmp { next_amp: u64, next_amp_time: u64 }, + StartChangingAmp { + next_amp: u64, + next_amp_time: u64, + }, StopChangingAmp {}, + /// Enables the sharing of swap fees with an external party. + EnableFeeShare { + /// The fee shared with the fee_share_address + fee_share_bps: u16, + /// The fee_share_bps is sent to this address on every swap + fee_share_address: String, + }, + DisableFeeShare, } #[cfg(test)] @@ -281,6 +315,7 @@ mod tests { params: Some( to_binary(&StablePoolConfig { amp: Decimal::one(), + fee_share: None, }) .unwrap(), ), diff --git a/packages/astroport/src/pair_concentrated.rs b/packages/astroport/src/pair_concentrated.rs index 6211f6812..ad49de885 100644 --- a/packages/astroport/src/pair_concentrated.rs +++ b/packages/astroport/src/pair_concentrated.rs @@ -5,8 +5,8 @@ use crate::asset::PairInfo; use crate::asset::{Asset, AssetInfo}; use crate::observation::OracleObservation; use crate::pair::{ - ConfigResponse, CumulativePricesResponse, PoolResponse, ReverseSimulationResponse, - SimulationResponse, + ConfigResponse, CumulativePricesResponse, FeeShareConfig, PoolResponse, + ReverseSimulationResponse, SimulationResponse, }; /// This structure holds concentrated pool parameters. @@ -68,6 +68,14 @@ pub enum ConcentratedPoolUpdateParams { StopChangingAmpGamma {}, /// Enable asset balances tracking EnableAssetBalancesTracking {}, + /// Enables the sharing of swap fees with an external party. + EnableFeeShare { + /// The fee shared with the fee_share_address + fee_share_bps: u16, + /// The fee_share_bps is sent to this address on every swap + fee_share_address: String, + }, + DisableFeeShare, } /// This structure stores a CL pool's configuration. @@ -95,6 +103,8 @@ pub struct ConcentratedPoolConfig { pub ma_half_time: u64, /// Whether asset balances are tracked over blocks or not. pub track_asset_balances: bool, + // The config for swap fee sharing + pub fee_share: Option, } /// This structure describes the query messages available in the contract.