From 70b0863124a357d8e88ced0981d4e3f465d60d2f Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 19 Sep 2024 13:23:41 +0100 Subject: [PATCH 01/21] fix: ignore quote metadata when deciding to to trigger a new quote --- .../src/common/updaters/FeesUpdater.ts | 41 +++++++++++++++++-- .../src/modules/appData/index.ts | 1 + 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts index e5e0fcce0b..6014f3953c 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts @@ -18,7 +18,7 @@ import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { useAppData } from 'modules/appData' +import { decodeAppData, useAppData } from 'modules/appData' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState' @@ -61,6 +61,7 @@ function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: Quot } = currentParams const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true + const hasSameAppData = compareAppDataWithoutQuoteData(appData, currentAppData) // cache the base quote params without quoteInfo user address to check const paramsWithoutAddress = @@ -68,20 +69,52 @@ function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: Quot buyToken === currentBuyToken && amount === currentAmount && kind === currentKind && - appData === currentAppData && + hasSameAppData && hasSameReceiver // 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without // in case user is not connected return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress } +/** + * Compares appData without taking into account the `quote` metadata + */ +function compareAppDataWithoutQuoteData(a: T, b: T): boolean { + if (a === b) { + return true + } + const cleanedA = removeQuoteMetadata(a) + const cleanedB = removeQuoteMetadata(b) + + return cleanedA === cleanedB +} + +/** + * If appData is set and is valid, remove `quote` metadata from it + */ +function removeQuoteMetadata(appData: string | undefined): string | undefined { + if (!appData) { + return + } + + const decoded = decodeAppData(appData) + + if (!decoded) { + return + } + + const { metadata: fullMetadata, ...rest } = decoded + const { quote: _, ...metadata } = fullMetadata + return JSON.stringify({ ...rest, metadata }) +} + /** * Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state) */ function isRefetchQuoteRequired( isLoading: boolean, currentParams: FeeQuoteParams, - quoteInformation?: QuoteInformationObject + quoteInformation?: QuoteInformationObject, ): boolean { // If there's no quote/fee information, we always re-fetch if (!quoteInformation) { @@ -219,7 +252,7 @@ export function FeesUpdater(): null { // Callback to re-fetch both the fee and the price const refetchQuoteIfRequired = () => { // if no token is unsupported and needs refetching - const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo) + const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo) // if (hasToRefetch) { // Decide if this is a new quote, or just a refresh diff --git a/apps/cowswap-frontend/src/modules/appData/index.ts b/apps/cowswap-frontend/src/modules/appData/index.ts index da9816331c..6e5d063d5d 100644 --- a/apps/cowswap-frontend/src/modules/appData/index.ts +++ b/apps/cowswap-frontend/src/modules/appData/index.ts @@ -6,5 +6,6 @@ export { decodeAppData } from './utils/decodeAppData' export { replaceHooksOnAppData, buildAppData, removePermitHookFromAppData } from './utils/buildAppData' export { buildAppDataHooks } from './utils/buildAppDataHooks' export * from './utils/getAppDataHooks' +export * from './utils/decodeAppData' export { addPermitHookToHooks, removePermitHookFromHooks } from './utils/typedHooks' export type { AppDataInfo, UploadAppDataParams, TypedAppDataHooks } from './types' From c599d793de090e03e92d4a9d4884926f5325dd0c Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 19 Sep 2024 13:24:13 +0100 Subject: [PATCH 02/21] fix: memoize useGetQuoteAndStatus response --- apps/cowswap-frontend/src/legacy/state/price/hooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts index b6f4ee84e6..6fd2e8206d 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' @@ -71,7 +71,7 @@ export const useGetQuoteAndStatus = (params: QuoteParams): UseGetQuoteAndStatus const isGettingNewQuote = Boolean(isLoading && !quote?.price?.amount) const isRefreshingQuote = Boolean(isLoading && quote?.price?.amount) - return { quote, isGettingNewQuote, isRefreshingQuote } + return useMemo(() => ({ quote, isGettingNewQuote, isRefreshingQuote }), [quote, isGettingNewQuote, isRefreshingQuote]) } export const useGetNewQuote = (): GetNewQuoteCallback => { From 4c01dd03443cad0c82bd7e6ccb4d463e28245054 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 19 Sep 2024 14:35:19 +0100 Subject: [PATCH 03/21] feat: use smart slippage based on trade size in relation to fee --- .../swap/updaters/SmartSlippageUpdater.ts | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index 7e79d89102..4a83fb3b42 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -1,5 +1,5 @@ import { useSetAtom } from 'jotai' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { BFF_BASE_URL } from '@cowprotocol/common-const' import { useFeatureFlags } from '@cowprotocol/common-hooks' @@ -11,6 +11,7 @@ import useSWR from 'swr' import { useDerivedTradeState, useIsWrapOrUnwrap } from 'modules/trade' +import { useDerivedSwapInfo, useHighFeeWarning } from '../hooks/useSwapState' import { smartSwapSlippageAtom } from '../state/slippageValueAndTypeAtom' const SWR_OPTIONS = { @@ -31,7 +32,7 @@ export function SmartSlippageUpdater() { const sellTokenAddress = inputCurrency && getCurrencyAddress(inputCurrency).toLowerCase() const buyTokenAddress = outputCurrency && getCurrencyAddress(outputCurrency).toLowerCase() - const slippageBps = useSWR( + const bffSlippageBps = useSWR( !sellTokenAddress || !buyTokenAddress || isWrapOrUnwrap || !isSmartSlippageEnabled ? null : [chainId, sellTokenAddress, buyTokenAddress], @@ -42,12 +43,53 @@ export function SmartSlippageUpdater() { return response.slippageBps }, - SWR_OPTIONS + SWR_OPTIONS, ).data + const tradeSizeSlippageBps = useSmartSlippageFromFeePercentage() + useEffect(() => { - setSmartSwapSlippage(typeof slippageBps === 'number' ? slippageBps : null) - }, [slippageBps, setSmartSwapSlippage]) + // Trade size slippage takes precedence + if (tradeSizeSlippageBps !== undefined) { + setSmartSwapSlippage(tradeSizeSlippageBps) + } else { + setSmartSwapSlippage(typeof bffSlippageBps === 'number' ? bffSlippageBps : null) + } + }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) return null } + +/** + * Calculates smart slippage in bps, based on trade size in relation to fee + */ +function useSmartSlippageFromFeePercentage(): number | undefined { + const { trade } = useDerivedSwapInfo() || {} + const { feePercentage } = useHighFeeWarning(trade) + + const percentage = feePercentage && +feePercentage.toFixed(3) + + return useMemo(() => { + if (percentage === undefined) { + // Unset, return undefined + return + } + if (percentage < 1) { + // bigger volume compared to the fee, trust on smart slippage from BFF + return + } else if (percentage < 5) { + // Between 1 and 5, 2% + return 200 + } else if (percentage < 10) { + // Between 5 and 10, 5% + return 500 + } else if (percentage < 20) { + // Between 10 and 20, 10% + return 1000 + } + // TODO: more granularity? + + // > 30%, cap it at 20% slippage + return 2000 + }, [percentage]) +} From 9e8d580c8a0396e41b06243468e02cc431858d4c Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 20 Sep 2024 17:25:24 +0100 Subject: [PATCH 04/21] feat: show loading indicator on suggested slippage when trade is loading --- .../swap/containers/Row/RowSlippage/index.tsx | 7 +++++-- .../src/modules/swap/hooks/useSwapState.tsx | 1 - .../pure/Row/RowSlippageContent/index.tsx | 21 ++++++++++++------- .../swap/updaters/SmartSlippageUpdater.ts | 2 +- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx index 681cec67ab..8faa466be3 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { formatPercent } from '@cowprotocol/common-utils' import { useWalletInfo } from '@cowprotocol/wallet' @@ -13,6 +13,7 @@ import { useSmartSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' import { RowSlippageContent } from 'modules/swap/pure/Row/RowSlippageContent' import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import { useTradePricesUpdate } from 'modules/swap/hooks/useTradePricesUpdate' export interface RowSlippageProps { allowedSlippage: Percent @@ -37,6 +38,7 @@ export function RowSlippage({ const smartSwapSlippage = useSmartSwapSlippage() const isSmartSlippageApplied = useIsSmartSlippageApplied() const setSlippage = useSetSlippage() + const isTradePriceUpdating = useTradePricesUpdate() const props = useMemo( () => ({ @@ -49,10 +51,11 @@ export function RowSlippage({ slippageTooltip, displaySlippage: `${formatPercent(allowedSlippage)}%`, isSmartSlippageApplied, + isSmartSlippageLoading: isTradePriceUpdating, smartSlippage: smartSwapSlippage && !isEoaEthFlow ? `${formatPercent(new Percent(smartSwapSlippage, 10_000))}%` : undefined, setAutoSlippage: smartSwapSlippage && !isEoaEthFlow ? () => setSlippage(null) : undefined, }), - [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied] + [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied, isTradePriceUpdating] ) return diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index c506885a3b..d0a98efd80 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -312,7 +312,6 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { } // compare input balance to max input based on version - // const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)] // mod const balanceIn = currencyBalances[Field.INPUT] const amountIn = slippageAdjustedSellAmount diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index a3385962ef..4f9cf5462c 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -1,7 +1,7 @@ import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' -import { HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' +import { CenteredDots, HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -82,6 +82,7 @@ export interface RowSlippageContentProps { setAutoSlippage?: Command // todo: make them optional smartSlippage?: string isSmartSlippageApplied: boolean + isSmartSlippageLoading: boolean } // TODO: RowDeadlineContent and RowSlippageContent are very similar. Refactor and extract base component? @@ -101,6 +102,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) { setAutoSlippage, smartSlippage, isSmartSlippageApplied, + isSmartSlippageLoading, } = props const tooltipContent = @@ -111,12 +113,17 @@ export function RowSlippageContent(props: RowSlippageContentProps) { const displayDefaultSlippage = isSlippageModified && setAutoSlippage && smartSlippage && !suggestedEqualToUserSlippage && ( - (Suggested: {smartSlippage}) - - - + {isSmartSlippageLoading ? () : ( + <> + (Suggested: {smartSlippage}) + + + + + )} ) + const loading = isSmartSlippageLoading && isSmartSlippageApplied && () return ( @@ -137,11 +144,11 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - {displaySlippage}{displayDefaultSlippage} + {loading ? loading : (<>{displaySlippage}{displayDefaultSlippage})} ) : ( - {displaySlippage}{displayDefaultSlippage} + {loading ? loading : (<>{displaySlippage}{displayDefaultSlippage})} )} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index 4a83fb3b42..2d52cb3073 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -89,7 +89,7 @@ function useSmartSlippageFromFeePercentage(): number | undefined { } // TODO: more granularity? - // > 30%, cap it at 20% slippage + // > 20%, cap it at 20% slippage return 2000 }, [percentage]) } From 6e7787e926b2edfc4bf67d86c145fd303aa6bd34 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 20 Sep 2024 17:31:24 +0100 Subject: [PATCH 05/21] chore: fix cosmos build --- .../modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx index 40671ff0f1..f180f5effb 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx @@ -19,6 +19,7 @@ const defaultProps: RowSlippageContentProps = { setAutoSlippage: () => { console.log('setAutoSlippage called!') }, + isSmartSlippageLoading: false } export default From ecb55eeed35d9c34ea62f7d26f5c3a841c971719 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Mon, 23 Sep 2024 17:24:04 +0100 Subject: [PATCH 06/21] chore: fix lint --- .../src/modules/swap/containers/Row/RowSlippage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx index 8faa466be3..812b97be9e 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react' +import { useMemo } from 'react' import { formatPercent } from '@cowprotocol/common-utils' import { useWalletInfo } from '@cowprotocol/wallet' @@ -10,10 +10,10 @@ import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' import { useSetSlippage } from 'modules/swap/hooks/useSetSlippage' import { useSmartSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' +import { useTradePricesUpdate } from 'modules/swap/hooks/useTradePricesUpdate' import { RowSlippageContent } from 'modules/swap/pure/Row/RowSlippageContent' import useNativeCurrency from 'lib/hooks/useNativeCurrency' -import { useTradePricesUpdate } from 'modules/swap/hooks/useTradePricesUpdate' export interface RowSlippageProps { allowedSlippage: Percent From 2796c1feaf4c0c6e9080c3818b0c772c7a929736 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Tue, 1 Oct 2024 18:14:39 +0100 Subject: [PATCH 07/21] feat: use smart slippage based on fee amount % --- .../swap/updaters/SmartSlippageUpdater.ts | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index 2d52cb3073..cbc7fda3ad 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -5,6 +5,7 @@ import { BFF_BASE_URL } from '@cowprotocol/common-const' import { useFeatureFlags } from '@cowprotocol/common-hooks' import { getCurrencyAddress } from '@cowprotocol/common-utils' import { useWalletInfo } from '@cowprotocol/wallet' +import { Fraction, TradeType } from '@uniswap/sdk-core' import ms from 'ms.macro' import useSWR from 'swr' @@ -46,20 +47,82 @@ export function SmartSlippageUpdater() { SWR_OPTIONS, ).data - const tradeSizeSlippageBps = useSmartSlippageFromFeePercentage() + // TODO: remove v1 + const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() + const tradeSizeSlippageBps = useSmartSlippageFromFeePercentageV2() useEffect(() => { - // Trade size slippage takes precedence - if (tradeSizeSlippageBps !== undefined) { - setSmartSwapSlippage(tradeSizeSlippageBps) - } else { - setSmartSwapSlippage(typeof bffSlippageBps === 'number' ? bffSlippageBps : null) - } + // Add both slippage values, when present + const slippage = tradeSizeSlippageBps + (bffSlippageBps || 0) + + setSmartSwapSlippage(slippage) }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) + // TODO: remove before merging + useEffect(() => { + console.log(`SmartSlippageUpdater`, { + granularSlippage: tradeSizeSlippageBpsV1, + fiftyPercentFeeSlippage: tradeSizeSlippageBps, + bffSlippageBps, + }) + }, [tradeSizeSlippageBpsV1, tradeSizeSlippageBps]) + return null } +const FEE_MULTIPLIER_FACTOR = new Fraction(15, 10) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction +const ONE = new Fraction(1) +const ZERO = new Fraction(0) + +/** + * Calculates smart slippage in bps, based on quoted fee + * + * Apply a multiplying factor to the fee (e.g.: 50%), and from there calculate how much slippage would be needed + * for the limit price to take this much more fee. + * More relevant for small orders in relation to fee amount, negligent for larger orders. + */ +function useSmartSlippageFromFeePercentageV2(): number { + const { trade } = useDerivedSwapInfo() || {} + const { fee, inputAmountWithFee, inputAmountWithoutFee, tradeType } = trade || {} + const { feeAsCurrency } = fee || {} + + const percentage = useMemo(() => { + if (!inputAmountWithFee || !inputAmountWithoutFee || !feeAsCurrency || tradeType === undefined) { + return ZERO + } + + if (tradeType === TradeType.EXACT_INPUT) { + // sell + // 1 - (sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount) + // 1 - (inputAmountWithoutFee - feeAsCurrency * 1.5) / inputAmountWithFee + return ONE.subtract( + inputAmountWithoutFee + .subtract(feeAsCurrency.multiply(FEE_MULTIPLIER_FACTOR)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(inputAmountWithFee.asFraction), + ) + } else { + // buy + // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 + // (inputAmountWithFee + feeAsCurrency * 1.5) / inputAmountWithFee - 1 + return ( + inputAmountWithFee + .add(feeAsCurrency.multiply(FEE_MULTIPLIER_FACTOR)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(inputAmountWithFee.asFraction) + .subtract(ONE) + ) + } + }, [tradeType, inputAmountWithFee, inputAmountWithoutFee, feeAsCurrency]) + + // Stable reference + // convert % to BPS. E.g.: 1% => 0.01 => 100 BPS + const bps = percentage.multiply(10_000).toFixed(0) + + return useMemo(() => +bps, [bps]) +} + +// TODO: remove /** * Calculates smart slippage in bps, based on trade size in relation to fee */ From d77ec8bc3ce4555a5b927a4d264f2acc9550e981 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 3 Oct 2024 18:30:59 +0100 Subject: [PATCH 08/21] feat: use fee multiplier factor from launch darkly --- .../src/modules/swap/updaters/SmartSlippageUpdater.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index cbc7fda3ad..7e17d4c5ed 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -70,7 +70,6 @@ export function SmartSlippageUpdater() { return null } -const FEE_MULTIPLIER_FACTOR = new Fraction(15, 10) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction const ONE = new Fraction(1) const ZERO = new Fraction(0) @@ -85,6 +84,8 @@ function useSmartSlippageFromFeePercentageV2(): number { const { trade } = useDerivedSwapInfo() || {} const { fee, inputAmountWithFee, inputAmountWithoutFee, tradeType } = trade || {} const { feeAsCurrency } = fee || {} + const { smartSlippageFeeMultiplierPercentage = 0 } = useFeatureFlags() + const feeMultiplierFactor = new Fraction(100 + smartSlippageFeeMultiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction const percentage = useMemo(() => { if (!inputAmountWithFee || !inputAmountWithoutFee || !feeAsCurrency || tradeType === undefined) { @@ -97,7 +98,7 @@ function useSmartSlippageFromFeePercentageV2(): number { // 1 - (inputAmountWithoutFee - feeAsCurrency * 1.5) / inputAmountWithFee return ONE.subtract( inputAmountWithoutFee - .subtract(feeAsCurrency.multiply(FEE_MULTIPLIER_FACTOR)) + .subtract(feeAsCurrency.multiply(feeMultiplierFactor)) // !!! Need to convert to fraction before division to not lose precision .asFraction.divide(inputAmountWithFee.asFraction), ) @@ -107,7 +108,7 @@ function useSmartSlippageFromFeePercentageV2(): number { // (inputAmountWithFee + feeAsCurrency * 1.5) / inputAmountWithFee - 1 return ( inputAmountWithFee - .add(feeAsCurrency.multiply(FEE_MULTIPLIER_FACTOR)) + .add(feeAsCurrency.multiply(feeMultiplierFactor)) // !!! Need to convert to fraction before division to not lose precision .asFraction.divide(inputAmountWithFee.asFraction) .subtract(ONE) From 7d99f389494b4af051a628bc3cc42941adb4927d Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 3 Oct 2024 18:35:41 +0100 Subject: [PATCH 09/21] refactor: exit earlier if LD multiplier is falsy --- .../modules/swap/updaters/SmartSlippageUpdater.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index 7e17d4c5ed..0b6024f7fb 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -88,14 +88,20 @@ function useSmartSlippageFromFeePercentageV2(): number { const feeMultiplierFactor = new Fraction(100 + smartSlippageFeeMultiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction const percentage = useMemo(() => { - if (!inputAmountWithFee || !inputAmountWithoutFee || !feeAsCurrency || tradeType === undefined) { + if ( + !inputAmountWithFee || + !inputAmountWithoutFee || + !feeAsCurrency || + tradeType === undefined || + !smartSlippageFeeMultiplierPercentage + ) { return ZERO } if (tradeType === TradeType.EXACT_INPUT) { // sell // 1 - (sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount) - // 1 - (inputAmountWithoutFee - feeAsCurrency * 1.5) / inputAmountWithFee + // 1 - (inputAmountWithoutFee - feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee return ONE.subtract( inputAmountWithoutFee .subtract(feeAsCurrency.multiply(feeMultiplierFactor)) @@ -105,7 +111,7 @@ function useSmartSlippageFromFeePercentageV2(): number { } else { // buy // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 - // (inputAmountWithFee + feeAsCurrency * 1.5) / inputAmountWithFee - 1 + // (inputAmountWithFee + feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee - 1 return ( inputAmountWithFee .add(feeAsCurrency.multiply(feeMultiplierFactor)) @@ -114,7 +120,7 @@ function useSmartSlippageFromFeePercentageV2(): number { .subtract(ONE) ) } - }, [tradeType, inputAmountWithFee, inputAmountWithoutFee, feeAsCurrency]) + }, [tradeType, inputAmountWithFee, inputAmountWithoutFee, feeAsCurrency, smartSlippageFeeMultiplierPercentage]) // Stable reference // convert % to BPS. E.g.: 1% => 0.01 => 100 BPS From c7214020f3022af7b3d50cbddb709dfa81db95f2 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 3 Oct 2024 18:42:31 +0100 Subject: [PATCH 10/21] fix: return undefined and avoid setting smart slippage when both are missing --- .../modules/swap/updaters/SmartSlippageUpdater.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index 0b6024f7fb..3e43c52686 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -52,8 +52,12 @@ export function SmartSlippageUpdater() { const tradeSizeSlippageBps = useSmartSlippageFromFeePercentageV2() useEffect(() => { + // If both are unset, don't use smart slippage + if (tradeSizeSlippageBps === undefined && bffSlippageBps === undefined) { + return + } // Add both slippage values, when present - const slippage = tradeSizeSlippageBps + (bffSlippageBps || 0) + const slippage = (tradeSizeSlippageBps || 0) + (bffSlippageBps || 0) setSmartSwapSlippage(slippage) }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) @@ -71,7 +75,6 @@ export function SmartSlippageUpdater() { } const ONE = new Fraction(1) -const ZERO = new Fraction(0) /** * Calculates smart slippage in bps, based on quoted fee @@ -80,7 +83,7 @@ const ZERO = new Fraction(0) * for the limit price to take this much more fee. * More relevant for small orders in relation to fee amount, negligent for larger orders. */ -function useSmartSlippageFromFeePercentageV2(): number { +function useSmartSlippageFromFeePercentageV2(): number | undefined { const { trade } = useDerivedSwapInfo() || {} const { fee, inputAmountWithFee, inputAmountWithoutFee, tradeType } = trade || {} const { feeAsCurrency } = fee || {} @@ -95,7 +98,7 @@ function useSmartSlippageFromFeePercentageV2(): number { tradeType === undefined || !smartSlippageFeeMultiplierPercentage ) { - return ZERO + return undefined } if (tradeType === TradeType.EXACT_INPUT) { @@ -124,9 +127,9 @@ function useSmartSlippageFromFeePercentageV2(): number { // Stable reference // convert % to BPS. E.g.: 1% => 0.01 => 100 BPS - const bps = percentage.multiply(10_000).toFixed(0) + const bps = percentage?.multiply(10_000).toFixed(0) - return useMemo(() => +bps, [bps]) + return useMemo(() => (bps ? +bps : undefined), [bps]) } // TODO: remove From 4a446538f8600a485a0ba7af358e2da08e5e85f8 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 3 Oct 2024 18:55:45 +0100 Subject: [PATCH 11/21] feat: cap slippage at 50% --- .../src/modules/swap/updaters/SmartSlippageUpdater.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index 3e43c52686..fdeaccbc36 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -75,6 +75,7 @@ export function SmartSlippageUpdater() { } const ONE = new Fraction(1) +const MAX_BPS = 5000 // 50% /** * Calculates smart slippage in bps, based on quoted fee @@ -129,7 +130,14 @@ function useSmartSlippageFromFeePercentageV2(): number | undefined { // convert % to BPS. E.g.: 1% => 0.01 => 100 BPS const bps = percentage?.multiply(10_000).toFixed(0) - return useMemo(() => (bps ? +bps : undefined), [bps]) + return useMemo(() => { + if (bps) { + // Cap it at MAX_BPS + return Math.min(+bps, MAX_BPS) + } else { + return undefined + } + }, [bps]) } // TODO: remove From d540b5b36e269cd0acb027cf87515a13c60fec28 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 08:52:42 +0100 Subject: [PATCH 12/21] refactor: split FeesUpdater --- .../{FeesUpdater.ts => FeesUpdater/index.ts} | 127 +----------------- .../FeesUpdater/isRefetchQuoteRequired.ts | 63 +++++++++ .../FeesUpdater/quoteUsingSameParameters.ts | 70 ++++++++++ 3 files changed, 137 insertions(+), 123 deletions(-) rename apps/cowswap-frontend/src/common/updaters/{FeesUpdater.ts => FeesUpdater/index.ts} (58%) create mode 100644 apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts create mode 100644 apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts similarity index 58% rename from apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts rename to apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts index 6014f3953c..a763129628 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts @@ -12,138 +12,19 @@ import ms from 'ms.macro' import { useRefetchQuoteCallback } from 'legacy/hooks/useRefetchPriceCallback' import { useAllQuotes, useIsBestQuoteLoading, useSetQuoteError } from 'legacy/state/price/hooks' -import { QuoteInformationObject } from 'legacy/state/price/reducer' -import { LegacyFeeQuoteParams } from 'legacy/state/price/types' import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { decodeAppData, useAppData } from 'modules/appData' +import { useAppData } from 'modules/appData' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState' +import { isRefetchQuoteRequired } from './isRefetchQuoteRequired' +import { quoteUsingSameParameters } from './quoteUsingSameParameters' + export const TYPED_VALUE_DEBOUNCE_TIME = 350 export const SWAP_QUOTE_CHECK_INTERVAL = ms`30s` // Every 30s -const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire -const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s) - -type FeeQuoteParams = Omit - -/** - * Returns if the quote has been recently checked - */ -function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean { - return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now() -} - -/** - * Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds) - */ -function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean { - const feeExpirationDate = Date.parse(quoteExpirationIsoDate) - return feeExpirationDate <= Date.now() + threshold -} - -/** - * Checks if the parameters for the current quote are correct - * - * Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched - */ -function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean { - const { - amount: currentAmount, - sellToken: currentSellToken, - buyToken: currentBuyToken, - kind: currentKind, - userAddress: currentUserAddress, - receiver: currentReceiver, - appData: currentAppData, - } = currentParams - const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo - const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true - const hasSameAppData = compareAppDataWithoutQuoteData(appData, currentAppData) - - // cache the base quote params without quoteInfo user address to check - const paramsWithoutAddress = - sellToken === currentSellToken && - buyToken === currentBuyToken && - amount === currentAmount && - kind === currentKind && - hasSameAppData && - hasSameReceiver - // 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without - // in case user is not connected - return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress -} - -/** - * Compares appData without taking into account the `quote` metadata - */ -function compareAppDataWithoutQuoteData(a: T, b: T): boolean { - if (a === b) { - return true - } - const cleanedA = removeQuoteMetadata(a) - const cleanedB = removeQuoteMetadata(b) - - return cleanedA === cleanedB -} - -/** - * If appData is set and is valid, remove `quote` metadata from it - */ -function removeQuoteMetadata(appData: string | undefined): string | undefined { - if (!appData) { - return - } - - const decoded = decodeAppData(appData) - - if (!decoded) { - return - } - - const { metadata: fullMetadata, ...rest } = decoded - const { quote: _, ...metadata } = fullMetadata - return JSON.stringify({ ...rest, metadata }) -} - -/** - * Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state) - */ -function isRefetchQuoteRequired( - isLoading: boolean, - currentParams: FeeQuoteParams, - quoteInformation?: QuoteInformationObject, -): boolean { - // If there's no quote/fee information, we always re-fetch - if (!quoteInformation) { - return true - } - - if (!quoteUsingSameParameters(currentParams, quoteInformation)) { - // If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched - return true - } - - // The query params are the same, so we only ask for a new quote if: - // - If the quote was not queried recently - // - There's not another price query going on right now - // - The quote will expire soon - if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) { - // Don't Re-fetch if it was queried recently - return false - } else if (isLoading) { - // Don't Re-fetch if there's another quote going on with the same params - // It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs - return false - } else if (quoteInformation.fee) { - // Re-fetch if the fee is expiring soon - return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME) - } - - return false -} export function FeesUpdater(): null { const { chainId, account } = useWalletInfo() diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts new file mode 100644 index 0000000000..5b4714631b --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts @@ -0,0 +1,63 @@ +import ms from 'ms.macro' + +import { QuoteInformationObject } from 'legacy/state/price/reducer' +import { LegacyFeeQuoteParams } from 'legacy/state/price/types' + +import { quoteUsingSameParameters } from './quoteUsingSameParameters' + +const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire +const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s) + +type FeeQuoteParams = Omit + +/** + * Returns if the quote has been recently checked + */ +function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean { + return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now() +} + +/** + * Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds) + */ +function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean { + const feeExpirationDate = Date.parse(quoteExpirationIsoDate) + return feeExpirationDate <= Date.now() + threshold +} + +/** + * Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state) + */ +export function isRefetchQuoteRequired( + isLoading: boolean, + currentParams: FeeQuoteParams, + quoteInformation?: QuoteInformationObject, +): boolean { + // If there's no quote/fee information, we always re-fetch + if (!quoteInformation) { + return true + } + + if (!quoteUsingSameParameters(currentParams, quoteInformation)) { + // If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched + return true + } + + // The query params are the same, so we only ask for a new quote if: + // - If the quote was not queried recently + // - There's not another price query going on right now + // - The quote will expire soon + if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) { + // Don't Re-fetch if it was queried recently + return false + } else if (isLoading) { + // Don't Re-fetch if there's another quote going on with the same params + // It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs + return false + } else if (quoteInformation.fee) { + // Re-fetch if the fee is expiring soon + return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME) + } + + return false +} diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts new file mode 100644 index 0000000000..b41f5e0a66 --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts @@ -0,0 +1,70 @@ +import { QuoteInformationObject } from 'legacy/state/price/reducer' +import { LegacyFeeQuoteParams } from 'legacy/state/price/types' + +import { decodeAppData } from 'modules/appData' + +type FeeQuoteParams = Omit + +/** + * Checks if the parameters for the current quote are correct + * + * Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched + */ +export function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean { + const { + amount: currentAmount, + sellToken: currentSellToken, + buyToken: currentBuyToken, + kind: currentKind, + userAddress: currentUserAddress, + receiver: currentReceiver, + appData: currentAppData, + } = currentParams + const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo + const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true + const hasSameAppData = compareAppDataWithoutQuoteData(appData, currentAppData) + + // cache the base quote params without quoteInfo user address to check + const paramsWithoutAddress = + sellToken === currentSellToken && + buyToken === currentBuyToken && + amount === currentAmount && + kind === currentKind && + hasSameAppData && + hasSameReceiver + // 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without + // in case user is not connected + return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress +} + +/** + * Compares appData without taking into account the `quote` metadata + */ +function compareAppDataWithoutQuoteData(a: T, b: T): boolean { + if (a === b) { + return true + } + const cleanedA = removeQuoteMetadata(a) + const cleanedB = removeQuoteMetadata(b) + + return cleanedA === cleanedB +} + +/** + * If appData is set and is valid, remove `quote` metadata from it + */ +function removeQuoteMetadata(appData: string | undefined): string | undefined { + if (!appData) { + return + } + + const decoded = decodeAppData(appData) + + if (!decoded) { + return + } + + const { metadata: fullMetadata, ...rest } = decoded + const { quote: _, ...metadata } = fullMetadata + return JSON.stringify({ ...rest, metadata }) +} From 0be600744a9958e51c6b8890942893f87d918c03 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 09:10:01 +0100 Subject: [PATCH 13/21] refactor: use useSafeMemoObject instead of useMemo --- apps/cowswap-frontend/src/legacy/state/price/hooks.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts index 6fd2e8206d..8bd90f0bca 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts @@ -1,9 +1,11 @@ -import { useCallback, useMemo } from 'react' +import { useCallback } from 'react' import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { useDispatch, useSelector } from 'react-redux' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' + import { ClearQuoteParams, getNewQuote, @@ -71,7 +73,11 @@ export const useGetQuoteAndStatus = (params: QuoteParams): UseGetQuoteAndStatus const isGettingNewQuote = Boolean(isLoading && !quote?.price?.amount) const isRefreshingQuote = Boolean(isLoading && quote?.price?.amount) - return useMemo(() => ({ quote, isGettingNewQuote, isRefreshingQuote }), [quote, isGettingNewQuote, isRefreshingQuote]) + return useSafeMemoObject({ + quote, + isGettingNewQuote, + isRefreshingQuote, + }) } export const useGetNewQuote = (): GetNewQuoteCallback => { From 44cad9ba4e11ed9d756913814c2e46273d7ba014 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 09:16:55 +0100 Subject: [PATCH 14/21] refactor: avoid repeating the same code --- .../pure/Row/RowSlippageContent/index.tsx | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index 4f9cf5462c..c1105bc66c 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -64,7 +64,8 @@ export const getNonNativeSlippageTooltip = () => ( ) -const SUGGESTED_SLIPPAGE_TOOLTIP = "Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order." +const SUGGESTED_SLIPPAGE_TOOLTIP = + 'Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order.' export interface RowSlippageContentProps { chainId: SupportedChainId @@ -111,19 +112,33 @@ export function RowSlippageContent(props: RowSlippageContentProps) { // In case the user happened to set the same slippage as the suggestion, do not show the suggestion const suggestedEqualToUserSlippage = smartSlippage && smartSlippage === displaySlippage - const displayDefaultSlippage = isSlippageModified && setAutoSlippage && smartSlippage && !suggestedEqualToUserSlippage && ( - - {isSmartSlippageLoading ? () : ( - <> - (Suggested: {smartSlippage}) - - - - - )} - - ) - const loading = isSmartSlippageLoading && isSmartSlippageApplied && () + const displayDefaultSlippage = isSlippageModified && + setAutoSlippage && + smartSlippage && + !suggestedEqualToUserSlippage && ( + + {isSmartSlippageLoading ? ( + + ) : ( + <> + (Suggested: {smartSlippage}) + + + + + )} + + ) + + const displaySlippageWithLoader = + isSmartSlippageLoading && isSmartSlippageApplied ? ( + + ) : ( + <> + {displaySlippage} + {displayDefaultSlippage} + + ) return ( @@ -131,10 +146,18 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - + ) : ( - + )} @@ -143,20 +166,20 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - - {loading ? loading : (<>{displaySlippage}{displayDefaultSlippage})} - + {displaySlippageWithLoader} ) : ( - - {loading ? loading : (<>{displaySlippage}{displayDefaultSlippage})} - + {displaySlippageWithLoader} )} ) } -type SlippageTextContentsProps = { isEoaEthFlow: boolean; slippageLabel?: React.ReactNode, isDynamicSlippageSet: boolean } +type SlippageTextContentsProps = { + isEoaEthFlow: boolean + slippageLabel?: React.ReactNode + isDynamicSlippageSet: boolean +} function SlippageTextContents({ isEoaEthFlow, slippageLabel, isDynamicSlippageSet }: SlippageTextContentsProps) { return ( From 26280dd0f371ac422fdecabb3421b4473194b252 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 10:43:33 +0100 Subject: [PATCH 15/21] refactor: split SmartSlippageUpdater --- .../swap/updaters/SmartSlippageUpdater.ts | 176 ------------------ .../updaters/SmartSlippageUpdater/index.ts | 74 ++++++++ .../useSmartSlippageFromBff.ts | 41 ++++ .../useSmartSlippageFromFeeMultiplier.ts | 72 +++++++ 4 files changed, 187 insertions(+), 176 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts deleted file mode 100644 index fdeaccbc36..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { useSetAtom } from 'jotai' -import { useEffect, useMemo } from 'react' - -import { BFF_BASE_URL } from '@cowprotocol/common-const' -import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { getCurrencyAddress } from '@cowprotocol/common-utils' -import { useWalletInfo } from '@cowprotocol/wallet' -import { Fraction, TradeType } from '@uniswap/sdk-core' - -import ms from 'ms.macro' -import useSWR from 'swr' - -import { useDerivedTradeState, useIsWrapOrUnwrap } from 'modules/trade' - -import { useDerivedSwapInfo, useHighFeeWarning } from '../hooks/useSwapState' -import { smartSwapSlippageAtom } from '../state/slippageValueAndTypeAtom' - -const SWR_OPTIONS = { - dedupingInterval: ms`1m`, -} - -interface SlippageApiResponse { - slippageBps: number -} - -export function SmartSlippageUpdater() { - const { isSmartSlippageEnabled } = useFeatureFlags() - const { chainId } = useWalletInfo() - const { inputCurrency, outputCurrency } = useDerivedTradeState() || {} - const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) - const isWrapOrUnwrap = useIsWrapOrUnwrap() - - const sellTokenAddress = inputCurrency && getCurrencyAddress(inputCurrency).toLowerCase() - const buyTokenAddress = outputCurrency && getCurrencyAddress(outputCurrency).toLowerCase() - - const bffSlippageBps = useSWR( - !sellTokenAddress || !buyTokenAddress || isWrapOrUnwrap || !isSmartSlippageEnabled - ? null - : [chainId, sellTokenAddress, buyTokenAddress], - async ([chainId, sellTokenAddress, buyTokenAddress]) => { - const url = `${BFF_BASE_URL}/${chainId}/markets/${sellTokenAddress}-${buyTokenAddress}/slippageTolerance` - - const response: SlippageApiResponse = await fetch(url).then((res) => res.json()) - - return response.slippageBps - }, - SWR_OPTIONS, - ).data - - // TODO: remove v1 - const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() - const tradeSizeSlippageBps = useSmartSlippageFromFeePercentageV2() - - useEffect(() => { - // If both are unset, don't use smart slippage - if (tradeSizeSlippageBps === undefined && bffSlippageBps === undefined) { - return - } - // Add both slippage values, when present - const slippage = (tradeSizeSlippageBps || 0) + (bffSlippageBps || 0) - - setSmartSwapSlippage(slippage) - }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) - - // TODO: remove before merging - useEffect(() => { - console.log(`SmartSlippageUpdater`, { - granularSlippage: tradeSizeSlippageBpsV1, - fiftyPercentFeeSlippage: tradeSizeSlippageBps, - bffSlippageBps, - }) - }, [tradeSizeSlippageBpsV1, tradeSizeSlippageBps]) - - return null -} - -const ONE = new Fraction(1) -const MAX_BPS = 5000 // 50% - -/** - * Calculates smart slippage in bps, based on quoted fee - * - * Apply a multiplying factor to the fee (e.g.: 50%), and from there calculate how much slippage would be needed - * for the limit price to take this much more fee. - * More relevant for small orders in relation to fee amount, negligent for larger orders. - */ -function useSmartSlippageFromFeePercentageV2(): number | undefined { - const { trade } = useDerivedSwapInfo() || {} - const { fee, inputAmountWithFee, inputAmountWithoutFee, tradeType } = trade || {} - const { feeAsCurrency } = fee || {} - const { smartSlippageFeeMultiplierPercentage = 0 } = useFeatureFlags() - const feeMultiplierFactor = new Fraction(100 + smartSlippageFeeMultiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction - - const percentage = useMemo(() => { - if ( - !inputAmountWithFee || - !inputAmountWithoutFee || - !feeAsCurrency || - tradeType === undefined || - !smartSlippageFeeMultiplierPercentage - ) { - return undefined - } - - if (tradeType === TradeType.EXACT_INPUT) { - // sell - // 1 - (sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount) - // 1 - (inputAmountWithoutFee - feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee - return ONE.subtract( - inputAmountWithoutFee - .subtract(feeAsCurrency.multiply(feeMultiplierFactor)) - // !!! Need to convert to fraction before division to not lose precision - .asFraction.divide(inputAmountWithFee.asFraction), - ) - } else { - // buy - // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 - // (inputAmountWithFee + feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee - 1 - return ( - inputAmountWithFee - .add(feeAsCurrency.multiply(feeMultiplierFactor)) - // !!! Need to convert to fraction before division to not lose precision - .asFraction.divide(inputAmountWithFee.asFraction) - .subtract(ONE) - ) - } - }, [tradeType, inputAmountWithFee, inputAmountWithoutFee, feeAsCurrency, smartSlippageFeeMultiplierPercentage]) - - // Stable reference - // convert % to BPS. E.g.: 1% => 0.01 => 100 BPS - const bps = percentage?.multiply(10_000).toFixed(0) - - return useMemo(() => { - if (bps) { - // Cap it at MAX_BPS - return Math.min(+bps, MAX_BPS) - } else { - return undefined - } - }, [bps]) -} - -// TODO: remove -/** - * Calculates smart slippage in bps, based on trade size in relation to fee - */ -function useSmartSlippageFromFeePercentage(): number | undefined { - const { trade } = useDerivedSwapInfo() || {} - const { feePercentage } = useHighFeeWarning(trade) - - const percentage = feePercentage && +feePercentage.toFixed(3) - - return useMemo(() => { - if (percentage === undefined) { - // Unset, return undefined - return - } - if (percentage < 1) { - // bigger volume compared to the fee, trust on smart slippage from BFF - return - } else if (percentage < 5) { - // Between 1 and 5, 2% - return 200 - } else if (percentage < 10) { - // Between 5 and 10, 5% - return 500 - } else if (percentage < 20) { - // Between 10 and 20, 10% - return 1000 - } - // TODO: more granularity? - - // > 20%, cap it at 20% slippage - return 2000 - }, [percentage]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts new file mode 100644 index 0000000000..c9206c383c --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -0,0 +1,74 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useMemo } from 'react' + +import { useSmartSlippageFromBff } from './useSmartSlippageFromBff' +import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMultiplier' + +import { useDerivedSwapInfo, useHighFeeWarning } from '../../hooks/useSwapState' +import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' + +export function SmartSlippageUpdater() { + const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) + + const bffSlippageBps = useSmartSlippageFromBff() + // TODO: remove v1 + const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() + const tradeSizeSlippageBps = useSmartSlippageFromFeeMultiplier() + + useEffect(() => { + // If both are unset, don't use smart slippage + if (tradeSizeSlippageBps === undefined && bffSlippageBps === undefined) { + return + } + // Add both slippage values, when present + const slippage = (tradeSizeSlippageBps || 0) + (bffSlippageBps || 0) + + setSmartSwapSlippage(slippage) + }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) + + // TODO: remove before merging + useEffect(() => { + console.log(`SmartSlippageUpdater`, { + granularSlippage: tradeSizeSlippageBpsV1, + fiftyPercentFeeSlippage: tradeSizeSlippageBps, + bffSlippageBps, + }) + }, [tradeSizeSlippageBpsV1, tradeSizeSlippageBps]) + + return null +} + +// TODO: remove +/** + * Calculates smart slippage in bps, based on trade size in relation to fee + */ +function useSmartSlippageFromFeePercentage(): number | undefined { + const { trade } = useDerivedSwapInfo() || {} + const { feePercentage } = useHighFeeWarning(trade) + + const percentage = feePercentage && +feePercentage.toFixed(3) + + return useMemo(() => { + if (percentage === undefined) { + // Unset, return undefined + return + } + if (percentage < 1) { + // bigger volume compared to the fee, trust on smart slippage from BFF + return + } else if (percentage < 5) { + // Between 1 and 5, 2% + return 200 + } else if (percentage < 10) { + // Between 5 and 10, 5% + return 500 + } else if (percentage < 20) { + // Between 10 and 20, 10% + return 1000 + } + // TODO: more granularity? + + // > 20%, cap it at 20% slippage + return 2000 + }, [percentage]) +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts new file mode 100644 index 0000000000..4891535205 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts @@ -0,0 +1,41 @@ +import { BFF_BASE_URL } from '@cowprotocol/common-const' +import { useFeatureFlags } from '@cowprotocol/common-hooks' +import { getCurrencyAddress } from '@cowprotocol/common-utils' +import { useWalletInfo } from '@cowprotocol/wallet' + +import ms from 'ms.macro' +import useSWR from 'swr' + +import { useDerivedTradeState, useIsWrapOrUnwrap } from '../../../trade' + +const SWR_OPTIONS = { + dedupingInterval: ms`1m`, +} + +interface SlippageApiResponse { + slippageBps: number +} + +export function useSmartSlippageFromBff(): number | undefined { + const { isSmartSlippageEnabled } = useFeatureFlags() + const { chainId } = useWalletInfo() + const { inputCurrency, outputCurrency } = useDerivedTradeState() || {} + const isWrapOrUnwrap = useIsWrapOrUnwrap() + + const sellTokenAddress = inputCurrency && getCurrencyAddress(inputCurrency).toLowerCase() + const buyTokenAddress = outputCurrency && getCurrencyAddress(outputCurrency).toLowerCase() + + return useSWR( + !sellTokenAddress || !buyTokenAddress || isWrapOrUnwrap || !isSmartSlippageEnabled + ? null + : [chainId, sellTokenAddress, buyTokenAddress], + async ([chainId, sellTokenAddress, buyTokenAddress]) => { + const url = `${BFF_BASE_URL}/${chainId}/markets/${sellTokenAddress}-${buyTokenAddress}/slippageTolerance` + + const response: SlippageApiResponse = await fetch(url).then((res) => res.json()) + + return response.slippageBps + }, + SWR_OPTIONS, + ).data +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts new file mode 100644 index 0000000000..3c8dc5827f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -0,0 +1,72 @@ +import { useMemo } from 'react' + +import { useFeatureFlags } from '@cowprotocol/common-hooks' +import { Fraction, TradeType } from '@uniswap/sdk-core' + +import { useDerivedSwapInfo } from '../../hooks/useSwapState' + +const ONE = new Fraction(1) +const MAX_BPS = 5000 // 50% + +/** + * Calculates smart slippage in bps, based on quoted fee + * + * Apply a multiplying factor to the fee (e.g.: 50%), and from there calculate how much slippage would be needed + * for the limit price to take this much more fee. + * More relevant for small orders in relation to fee amount, negligent for larger orders. + */ +export function useSmartSlippageFromFeeMultiplier(): number | undefined { + const { trade } = useDerivedSwapInfo() || {} + const { fee, inputAmountWithFee, inputAmountWithoutFee, tradeType } = trade || {} + const { feeAsCurrency } = fee || {} + const { smartSlippageFeeMultiplierPercentage = 0 } = useFeatureFlags() + const feeMultiplierFactor = new Fraction(100 + smartSlippageFeeMultiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction + + const percentage = useMemo(() => { + if ( + !inputAmountWithFee || + !inputAmountWithoutFee || + !feeAsCurrency || + tradeType === undefined || + !smartSlippageFeeMultiplierPercentage + ) { + return undefined + } + + if (tradeType === TradeType.EXACT_INPUT) { + // sell + // 1 - (sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount) + // 1 - (inputAmountWithoutFee - feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee + return ONE.subtract( + inputAmountWithoutFee + .subtract(feeAsCurrency.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(inputAmountWithFee.asFraction), + ) + } else { + // buy + // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 + // (inputAmountWithFee + feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee - 1 + return ( + inputAmountWithFee + .add(feeAsCurrency.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(inputAmountWithFee.asFraction) + .subtract(ONE) + ) + } + }, [tradeType, inputAmountWithFee, inputAmountWithoutFee, feeAsCurrency, smartSlippageFeeMultiplierPercentage]) + + // Stable reference + // convert % to BPS. E.g.: 1% => 0.01 => 100 BPS + const bps = percentage?.multiply(10_000).toFixed(0) + + return useMemo(() => { + if (bps) { + // Cap it at MAX_BPS + return Math.min(+bps, MAX_BPS) + } else { + return undefined + } + }, [bps]) +} From c964b9f5bf53af00be0b28157315189da317cc67 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 16:27:00 +0100 Subject: [PATCH 16/21] refactor: extract calculateBpsFromFeeMultiplier and added unittests --- .../calculateBpsFromFeeMultiplier.test.ts | 51 ++++++++++++++ .../calculateBpsFromFeeMultiplier.ts | 47 +++++++++++++ .../useSmartSlippageFromFeeMultiplier.ts | 66 ++++--------------- 3 files changed, 109 insertions(+), 55 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts new file mode 100644 index 0000000000..d2499acddc --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts @@ -0,0 +1,51 @@ +import { USDC } from '@cowprotocol/common-const' +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { calculateBpsFromFeeMultiplier } from './calculateBpsFromFeeMultiplier' + +describe('calculateBpsFromFeeMultiplier', () => { + const sellAmount = CurrencyAmount.fromRawAmount(USDC[1], '1000000') // 1.2 USDC + const feeAmount = CurrencyAmount.fromRawAmount(USDC[1], '200000') // 0.2 USDC + const isSell = true + const multiplierPercentage = 50 + + it('should return undefined for missing parameters', () => { + expect(calculateBpsFromFeeMultiplier(undefined, feeAmount, isSell, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, undefined, isSell, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, feeAmount, undefined, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, 0)).toBeUndefined() + }) + + it('should return undefined for a negative multiplier percentage', () => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, -50) + expect(result).toBeUndefined() + }) + + it('should calculate the correct percentage for selling with different multiplier percentages', () => { + const testCases = [ + [25, 625], // 25%, 6.25% + [50, 1250], // 50%, 12.5% + [75, 1875], // 75%, 18.75% + ] + + testCases.forEach(([multiplier, expectedResult]) => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, multiplier) + expect(result).toBeDefined() + expect(result).toBe(expectedResult) + }) + }) + + it('should calculate the correct percentage for buying with different multiplier percentages', () => { + const testCases = [ + [25, 417], // 25%, 4.17% + [50, 833], // 50%, 8.33% + [75, 1250], // 75%, 12.5% + ] + + testCases.forEach(([multiplier, expectedResult]) => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, !isSell, multiplier) + expect(result).toBeDefined() + expect(result).toBe(expectedResult) + }) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts new file mode 100644 index 0000000000..73d161d60d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts @@ -0,0 +1,47 @@ +import { Currency, CurrencyAmount, Fraction } from '@uniswap/sdk-core' + +const ONE = new Fraction(1) + +export function calculateBpsFromFeeMultiplier( + sellAmount: CurrencyAmount | undefined, + feeAmount: CurrencyAmount | undefined, + isSell: boolean | undefined, + multiplierPercentage: number, +): number | undefined { + if (!sellAmount || !feeAmount || isSell === undefined || multiplierPercentage <= 0) { + return undefined + } + + const feeMultiplierFactor = new Fraction(100 + multiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction + + if (isSell) { + // sell + // 1 - ((sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount)) + // 1 - (sellAmount - feeAmount * feeMultiplierFactor) / sellAmount - feeAmount + return percentageToBps( + ONE.subtract( + sellAmount + .subtract(feeAmount.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(sellAmount.subtract(feeAmount).asFraction), + ), + ) + } else { + // buy + // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 + // ((sellAmount + feeAmount * feeMultiplierFactor) / (sellAmount - feeAmount)) - 1 + return percentageToBps( + sellAmount + .add(feeAmount.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(sellAmount.add(feeAmount).asFraction) + .subtract(ONE), + ) + } +} + +function percentageToBps(value: Fraction | undefined): number | undefined { + const bps = value?.multiply(10_000).toFixed(0) + + return bps ? +bps : undefined +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts index 3c8dc5827f..8efc67477a 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -1,12 +1,10 @@ import { useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { Fraction, TradeType } from '@uniswap/sdk-core' -import { useDerivedSwapInfo } from '../../hooks/useSwapState' +import { calculateBpsFromFeeMultiplier } from './calculateBpsFromFeeMultiplier' -const ONE = new Fraction(1) -const MAX_BPS = 5000 // 50% +import { useReceiveAmountInfo } from '../../../trade' /** * Calculates smart slippage in bps, based on quoted fee @@ -16,57 +14,15 @@ const MAX_BPS = 5000 // 50% * More relevant for small orders in relation to fee amount, negligent for larger orders. */ export function useSmartSlippageFromFeeMultiplier(): number | undefined { - const { trade } = useDerivedSwapInfo() || {} - const { fee, inputAmountWithFee, inputAmountWithoutFee, tradeType } = trade || {} - const { feeAsCurrency } = fee || {} - const { smartSlippageFeeMultiplierPercentage = 0 } = useFeatureFlags() - const feeMultiplierFactor = new Fraction(100 + smartSlippageFeeMultiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction + const { beforeNetworkCosts, costs, isSell } = useReceiveAmountInfo() || {} + const { sellAmount: sellAmount } = beforeNetworkCosts || {} + const { networkFee } = costs || {} + const { amountInSellCurrency: feeAmount } = networkFee || {} - const percentage = useMemo(() => { - if ( - !inputAmountWithFee || - !inputAmountWithoutFee || - !feeAsCurrency || - tradeType === undefined || - !smartSlippageFeeMultiplierPercentage - ) { - return undefined - } + const { smartSlippageFeeMultiplierPercentage = 50 } = useFeatureFlags() - if (tradeType === TradeType.EXACT_INPUT) { - // sell - // 1 - (sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount) - // 1 - (inputAmountWithoutFee - feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee - return ONE.subtract( - inputAmountWithoutFee - .subtract(feeAsCurrency.multiply(feeMultiplierFactor)) - // !!! Need to convert to fraction before division to not lose precision - .asFraction.divide(inputAmountWithFee.asFraction), - ) - } else { - // buy - // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 - // (inputAmountWithFee + feeAsCurrency * feeMultiplierFactor) / inputAmountWithFee - 1 - return ( - inputAmountWithFee - .add(feeAsCurrency.multiply(feeMultiplierFactor)) - // !!! Need to convert to fraction before division to not lose precision - .asFraction.divide(inputAmountWithFee.asFraction) - .subtract(ONE) - ) - } - }, [tradeType, inputAmountWithFee, inputAmountWithoutFee, feeAsCurrency, smartSlippageFeeMultiplierPercentage]) - - // Stable reference - // convert % to BPS. E.g.: 1% => 0.01 => 100 BPS - const bps = percentage?.multiply(10_000).toFixed(0) - - return useMemo(() => { - if (bps) { - // Cap it at MAX_BPS - return Math.min(+bps, MAX_BPS) - } else { - return undefined - } - }, [bps]) + return useMemo( + () => calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, smartSlippageFeeMultiplierPercentage), + [isSell, sellAmount, feeAmount, smartSlippageFeeMultiplierPercentage], + ) } From 231ac6ecaf54d826fa7b1a64a66a971f963cf5b7 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 16:27:18 +0100 Subject: [PATCH 17/21] feat: cap the sum of calculated slippages at 50% --- .../src/modules/swap/updaters/SmartSlippageUpdater/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts index c9206c383c..48e8673acc 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -7,6 +7,8 @@ import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMult import { useDerivedSwapInfo, useHighFeeWarning } from '../../hooks/useSwapState' import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' +const MAX_BPS = 5000 // 50% + export function SmartSlippageUpdater() { const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) @@ -23,7 +25,7 @@ export function SmartSlippageUpdater() { // Add both slippage values, when present const slippage = (tradeSizeSlippageBps || 0) + (bffSlippageBps || 0) - setSmartSwapSlippage(slippage) + setSmartSwapSlippage(Math.min(slippage, MAX_BPS)) }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) // TODO: remove before merging From f57253ca665a7c30d5ab7b619c063b0f65bde0cc Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 16:28:31 +0100 Subject: [PATCH 18/21] refactor: rename variable to calculateBpsFromFeeMultiplier --- .../swap/updaters/SmartSlippageUpdater/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts index 48e8673acc..3a801bbf15 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -15,27 +15,27 @@ export function SmartSlippageUpdater() { const bffSlippageBps = useSmartSlippageFromBff() // TODO: remove v1 const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() - const tradeSizeSlippageBps = useSmartSlippageFromFeeMultiplier() + const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() useEffect(() => { // If both are unset, don't use smart slippage - if (tradeSizeSlippageBps === undefined && bffSlippageBps === undefined) { + if (feeMultiplierSlippageBps === undefined && bffSlippageBps === undefined) { return } // Add both slippage values, when present - const slippage = (tradeSizeSlippageBps || 0) + (bffSlippageBps || 0) + const slippage = (feeMultiplierSlippageBps || 0) + (bffSlippageBps || 0) setSmartSwapSlippage(Math.min(slippage, MAX_BPS)) - }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) + }, [bffSlippageBps, setSmartSwapSlippage, feeMultiplierSlippageBps]) // TODO: remove before merging useEffect(() => { console.log(`SmartSlippageUpdater`, { granularSlippage: tradeSizeSlippageBpsV1, - fiftyPercentFeeSlippage: tradeSizeSlippageBps, + fiftyPercentFeeSlippage: feeMultiplierSlippageBps, bffSlippageBps, }) - }, [tradeSizeSlippageBpsV1, tradeSizeSlippageBps]) + }, [tradeSizeSlippageBpsV1, feeMultiplierSlippageBps]) return null } From ccd5c199085e734f729427edf702bfa6ef1e1b47 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 16:44:46 +0100 Subject: [PATCH 19/21] fix: fix before and after as they don't mean the same thing for buy and sell orders --- .../useSmartSlippageFromFeeMultiplier.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts index 8efc67477a..ffefec77fe 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -14,10 +14,9 @@ import { useReceiveAmountInfo } from '../../../trade' * More relevant for small orders in relation to fee amount, negligent for larger orders. */ export function useSmartSlippageFromFeeMultiplier(): number | undefined { - const { beforeNetworkCosts, costs, isSell } = useReceiveAmountInfo() || {} - const { sellAmount: sellAmount } = beforeNetworkCosts || {} - const { networkFee } = costs || {} - const { amountInSellCurrency: feeAmount } = networkFee || {} + const { beforeNetworkCosts, afterNetworkCosts, costs, isSell } = useReceiveAmountInfo() || {} + const sellAmount = isSell ? afterNetworkCosts?.sellAmount : beforeNetworkCosts?.sellAmount + const feeAmount = costs?.networkFee?.amountInSellCurrency const { smartSlippageFeeMultiplierPercentage = 50 } = useFeatureFlags() From 1fbd9dd79c41451222294466a6cf4072e39257d6 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 16:46:48 +0100 Subject: [PATCH 20/21] fix: reset smart slippage when both are disabled --- .../src/modules/swap/updaters/SmartSlippageUpdater/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts index 3a801bbf15..33eb61af45 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -20,6 +20,7 @@ export function SmartSlippageUpdater() { useEffect(() => { // If both are unset, don't use smart slippage if (feeMultiplierSlippageBps === undefined && bffSlippageBps === undefined) { + setSmartSwapSlippage(null) return } // Add both slippage values, when present From b6a42081aa14d2862e1efe6d63a23581f6b926f7 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 4 Oct 2024 18:15:22 +0100 Subject: [PATCH 21/21] fix: don't update smart slippage if trade review modal is open --- .../modules/swap/updaters/SmartSlippageUpdater/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts index 33eb61af45..0a3398cc2e 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -6,6 +6,7 @@ import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMult import { useDerivedSwapInfo, useHighFeeWarning } from '../../hooks/useSwapState' import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' +import { useTradeConfirmState } from '../../../trade' const MAX_BPS = 5000 // 50% @@ -17,7 +18,13 @@ export function SmartSlippageUpdater() { const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() + const { isOpen: isTradeReviewOpen } = useTradeConfirmState() + useEffect(() => { + // Don't update it once review is open + if (isTradeReviewOpen) { + return + } // If both are unset, don't use smart slippage if (feeMultiplierSlippageBps === undefined && bffSlippageBps === undefined) { setSmartSwapSlippage(null) @@ -27,7 +34,7 @@ export function SmartSlippageUpdater() { const slippage = (feeMultiplierSlippageBps || 0) + (bffSlippageBps || 0) setSmartSwapSlippage(Math.min(slippage, MAX_BPS)) - }, [bffSlippageBps, setSmartSwapSlippage, feeMultiplierSlippageBps]) + }, [bffSlippageBps, setSmartSwapSlippage, feeMultiplierSlippageBps, isTradeReviewOpen]) // TODO: remove before merging useEffect(() => {