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): small order slippage v2 #4934

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
70b0863
fix: ignore quote metadata when deciding to to trigger a new quote
alfetopito Sep 19, 2024
c599d79
fix: memoize useGetQuoteAndStatus response
alfetopito Sep 19, 2024
4c01dd0
feat: use smart slippage based on trade size in relation to fee
alfetopito Sep 19, 2024
9e8d580
feat: show loading indicator on suggested slippage when trade is loading
alfetopito Sep 20, 2024
6e7787e
chore: fix cosmos build
alfetopito Sep 20, 2024
ecb55ee
chore: fix lint
alfetopito Sep 23, 2024
2796c1f
feat: use smart slippage based on fee amount %
alfetopito Oct 1, 2024
d77ec8b
feat: use fee multiplier factor from launch darkly
alfetopito Oct 3, 2024
7d99f38
refactor: exit earlier if LD multiplier is falsy
alfetopito Oct 3, 2024
c721402
fix: return undefined and avoid setting smart slippage when both are …
alfetopito Oct 3, 2024
4a44653
feat: cap slippage at 50%
alfetopito Oct 3, 2024
d540b5b
refactor: split FeesUpdater
alfetopito Oct 4, 2024
0be6007
refactor: use useSafeMemoObject instead of useMemo
alfetopito Oct 4, 2024
44cad9b
refactor: avoid repeating the same code
alfetopito Oct 4, 2024
26280dd
refactor: split SmartSlippageUpdater
alfetopito Oct 4, 2024
c964b9f
refactor: extract calculateBpsFromFeeMultiplier and added unittests
alfetopito Oct 4, 2024
231ac6e
feat: cap the sum of calculated slippages at 50%
alfetopito Oct 4, 2024
f57253c
refactor: rename variable to calculateBpsFromFeeMultiplier
alfetopito Oct 4, 2024
ccd5c19
fix: fix before and after as they don't mean the same thing for buy a…
alfetopito Oct 4, 2024
1fbd9dd
fix: reset smart slippage when both are disabled
alfetopito Oct 4, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ 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'
Expand All @@ -22,95 +20,11 @@ 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<LegacyFeeQuoteParams, 'validTo'>

/**
* 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

// cache the base quote params without quoteInfo user address to check
const paramsWithoutAddress =
sellToken === currentSellToken &&
buyToken === currentBuyToken &&
amount === currentAmount &&
kind === currentKind &&
appData === currentAppData &&
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
}

/**
* 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()
Expand Down Expand Up @@ -219,7 +133,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
Original file line number Diff line number Diff line change
@@ -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<LegacyFeeQuoteParams, 'validTo'>

/**
* 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
}
Original file line number Diff line number Diff line change
@@ -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<LegacyFeeQuoteParams, 'validTo'>

/**
* 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<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 })
}
8 changes: 7 additions & 1 deletion apps/cowswap-frontend/src/legacy/state/price/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk'

import { useDispatch, useSelector } from 'react-redux'

import { useSafeMemoObject } from 'common/hooks/useSafeMemo'

import {
ClearQuoteParams,
getNewQuote,
Expand Down Expand Up @@ -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 { quote, isGettingNewQuote, isRefreshingQuote }
return useSafeMemoObject({
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 @@ -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'
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} />
Loading
Loading