Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(slippage): smart slippage for high fee orders #4911

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -61,27 +61,60 @@ 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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to drop the quote from appData otherwise it enters into an infinite loop.


// cache the base quote params without quoteInfo user address to check
const paramsWithoutAddress =
sellToken === currentSellToken &&
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<T extends string | undefined>(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) {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/cowswap-frontend/src/legacy/state/price/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'

import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk'

Expand Down Expand Up @@ -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 => {
Expand Down
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/modules/appData/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export { filterPermitSignerPermit } from './utils/appDataFilter'
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'
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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'
Expand Down Expand Up @@ -37,6 +38,7 @@ export function RowSlippage({
const smartSwapSlippage = useSmartSwapSlippage()
const isSmartSlippageApplied = useIsSmartSlippageApplied()
const setSlippage = useSetSlippage()
const isTradePriceUpdating = useTradePricesUpdate()

const props = useMemo(
() => ({
Expand All @@ -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 <RowSlippageContent {...props} toggleSettings={toggleSettings} isSlippageModified={isSlippageModified} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const defaultProps: RowSlippageContentProps = {
setAutoSlippage: () => {
console.log('setAutoSlippage called!')
},
isSmartSlippageLoading: false
}

export default <RowSlippageContent {...defaultProps} />
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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?
Expand All @@ -101,6 +102,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) {
setAutoSlippage,
smartSlippage,
isSmartSlippageApplied,
isSmartSlippageLoading,
} = props

const tooltipContent =
Expand All @@ -111,12 +113,17 @@ export function RowSlippageContent(props: RowSlippageContentProps) {

const displayDefaultSlippage = isSlippageModified && setAutoSlippage && smartSlippage && !suggestedEqualToUserSlippage && (
<DefaultSlippage>
<LinkStyledButton onClick={setAutoSlippage}>(Suggested: {smartSlippage})</LinkStyledButton>
<HoverTooltip wrapInContainer content={SUGGESTED_SLIPPAGE_TOOLTIP}>
<StyledInfoIcon size={16} />
</HoverTooltip>
{isSmartSlippageLoading ? (<CenteredDots />) : (
<>
<LinkStyledButton onClick={setAutoSlippage}>(Suggested: {smartSlippage})</LinkStyledButton>
<HoverTooltip wrapInContainer content={SUGGESTED_SLIPPAGE_TOOLTIP}>
<StyledInfoIcon size={16} />
</HoverTooltip>
</>
)}
</DefaultSlippage>
)
const loading = isSmartSlippageLoading && isSmartSlippageApplied && (<CenteredDots />)

return (
<StyledRowBetween {...styleProps}>
Expand All @@ -137,11 +144,11 @@ export function RowSlippageContent(props: RowSlippageContentProps) {
<TextWrapper textAlign="right">
{showSettingOnClick ? (
<ClickableText onClick={toggleSettings}>
{displaySlippage}{displayDefaultSlippage}
{loading ? loading : (<>{displaySlippage}{displayDefaultSlippage}</>)}
</ClickableText>
) : (
<span>
{displaySlippage}{displayDefaultSlippage}
{loading ? loading : (<>{displaySlippage}{displayDefaultSlippage}</>)}
</span>
)}
</TextWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 = {
Expand All @@ -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],
Expand All @@ -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?

// > 20%, cap it at 20% slippage
return 2000
}, [percentage])
}
Loading