Commit 1c142bb7 authored by cartcrom's avatar cartcrom Committed by GitHub

feat: workaround for FOT swaps (#7218)

* fix: workaround to include tax in slippage tolerance

* refactor: simplify tax line item components

* docs: update FOT comments

* fix: simplify tax calculation logic

* docs: update comment and add TODO

* fix: use postTaxAmountOut for price change calculation

* fix: use preTaxStablecoinPriceImpact for swab button warnings

* test: unit test for swap details/modal

* feat: add feature flag to gate FOT changes

* docs: add comment to postTaxOutputAmount getter

* fix: reword fee tooltip

* refactor: warning theme color variable usage

* fix: update snapshots

* fix: use preTaxStablecoinPriceImpact for price impact prompt logic

* lint: dependency array
parent 3e67982b
......@@ -90,7 +90,7 @@ const DescriptionText = styled(ThemedText.LabelMicro)`
function useOrderAmounts(
orderDetails?: UniswapXOrderDetails
): Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> | undefined {
): Pick<InterfaceTrade, 'inputAmount' | 'postTaxOutputAmount'> | undefined {
const inputCurrency = useCurrency(orderDetails?.swapInfo?.inputCurrencyId, orderDetails?.chainId)
const outputCurrency = useCurrency(orderDetails?.swapInfo?.outputCurrencyId, orderDetails?.chainId)
......@@ -106,7 +106,7 @@ function useOrderAmounts(
if (swapInfo.tradeType === TradeType.EXACT_INPUT) {
return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.inputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount(
postTaxOutputAmount: CurrencyAmount.fromRawAmount(
outputCurrency,
swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw
),
......@@ -114,7 +114,7 @@ function useOrderAmounts(
} else {
return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.expectedInputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount(outputCurrency, swapInfo.outputCurrencyAmountRaw),
postTaxOutputAmount: CurrencyAmount.fromRawAmount(outputCurrency, swapInfo.outputCurrencyAmountRaw),
}
}
}
......
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion'
import { useForceUniswapXOnFlag } from 'featureFlags/flags/forceUniswapXOn'
import { useFotAdjustmentsFlag } from 'featureFlags/flags/fotAdjustments'
import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { UniswapXVariant, useUniswapXFlag } from 'featureFlags/flags/uniswapx'
......@@ -242,6 +243,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.multichainUX}
label="Updated Multichain UX"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useFotAdjustmentsFlag()}
featureFlag={FeatureFlag.fotAdjustedmentsEnabled}
label="Enable fee-on-transfer UI and slippage adjustments"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}
......
......@@ -5,8 +5,9 @@ import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { ZERO_PERCENT } from 'constants/misc'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { InterfaceTrade } from 'state/routing/types'
import { ClassicTrade, InterfaceTrade } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils'
import { formatCurrencyAmount, formatNumber, formatPriceImpact, NumberType } from 'utils/formatNumbers'
......@@ -82,16 +83,20 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</RowBetween>
)}
{isClassicTrade(trade) && (
<RowBetween>
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<ThemedText.BodySmall color="textSecondary">
<Trans>Price Impact</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>{formatPriceImpact(trade.priceImpact)}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
<>
<TokenTaxLineItem trade={trade} type="input" />
<TokenTaxLineItem trade={trade} type="output" />
<RowBetween>
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<ThemedText.BodySmall color="textSecondary">
<Trans>Price Impact</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>{formatPriceImpact(trade.priceImpact)}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
</>
)}
<RowBetween>
<RowFixed>
......@@ -135,7 +140,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.BodySmall>
{`${formatCurrencyAmount(trade.outputAmount, NumberType.SwapTradeAmount)} ${
{`${formatCurrencyAmount(trade.postTaxOutputAmount, NumberType.SwapTradeAmount)} ${
trade.outputAmount.currency.symbol
}`}
</ThemedText.BodySmall>
......@@ -176,3 +181,26 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</Column>
)
}
function TokenTaxLineItem({ trade, type }: { trade: ClassicTrade; type: 'input' | 'output' }) {
const [currency, percentage] =
type === 'input' ? [trade.inputAmount.currency, trade.inputTax] : [trade.outputAmount.currency, trade.outputTax]
if (percentage.equalTo(ZERO_PERCENT)) return null
return (
<RowBetween>
<MouseoverTooltip
text={
<Trans>
Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not
receive any of these fees.
</Trans>
}
>
<ThemedText.BodySmall color="textSecondary">{`${currency.symbol} fee`}</ThemedText.BodySmall>
</MouseoverTooltip>
<ThemedText.BodySmall>{formatPriceImpact(percentage)}</ThemedText.BodySmall>
</RowBetween>
)
}
......@@ -304,6 +304,7 @@ export default function ConfirmSwapModal({
// Swap failed locally and was not broadcast to the blockchain.
const localSwapFailure = Boolean(swapError) && !didUserReject(swapError)
const swapFailed = localSwapFailure || swapReverted
useEffect(() => {
// Reset the modal state if the user rejected the swap.
if (swapError && !swapFailed) {
......
......@@ -6,7 +6,7 @@ import { useTheme } from 'styled-components'
import { ThemedText } from 'theme'
import { formatReviewSwapCurrencyAmount } from 'utils/formatNumbers'
export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> }) {
export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmount' | 'postTaxOutputAmount'> }) {
const theme = useTheme()
return (
<Row gap="sm" justify="center" align="center">
......@@ -15,9 +15,9 @@ export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmou
{formatReviewSwapCurrencyAmount(trade.inputAmount)} {trade.inputAmount.currency.symbol}
</ThemedText.LabelSmall>
<ArrowRight color={theme.textPrimary} size="12px" />
<CurrencyLogo currency={trade.outputAmount.currency} size="16px" />
<CurrencyLogo currency={trade.postTaxOutputAmount.currency} size="16px" />
<ThemedText.LabelSmall color="textPrimary">
{formatReviewSwapCurrencyAmount(trade.outputAmount)} {trade.outputAmount.currency.symbol}
{formatReviewSwapCurrencyAmount(trade.postTaxOutputAmount)} {trade.postTaxOutputAmount.currency.symbol}
</ThemedText.LabelSmall>
</Row>
)
......
import userEvent from '@testing-library/user-event'
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import {
TEST_ALLOWED_SLIPPAGE,
TEST_TOKEN_1,
TEST_TOKEN_2,
TEST_TRADE_EXACT_INPUT,
TEST_TRADE_FEE_ON_BUY,
TEST_TRADE_FEE_ON_SELL,
} from 'test-utils/constants'
import { act, render, screen } from 'test-utils/render'
import SwapDetailsDropdown from './SwapDetailsDropdown'
......@@ -39,4 +46,42 @@ describe('SwapDetailsDropdown.tsx', () => {
await act(() => userEvent.click(screen.getByTestId('swap-details-header-row')))
expect(screen.getByTestId('advanced-swap-details')).toBeInTheDocument()
})
it('renders fee on input transfer information', async () => {
render(
<SwapDetailsDropdown
trade={TEST_TRADE_FEE_ON_SELL}
syncing={true}
loading={true}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
/>
)
await act(() => userEvent.click(screen.getByTestId('swap-details-header-row')))
expect(
screen.getByText(
'Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any of these fees.'
)
).toBeInTheDocument()
expect(screen.getByText(`${TEST_TOKEN_1.symbol} fee`)).toBeInTheDocument()
})
it('renders fee on ouput transfer information', async () => {
render(
<SwapDetailsDropdown
trade={TEST_TRADE_FEE_ON_BUY}
syncing={true}
loading={true}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
/>
)
await act(() => userEvent.click(screen.getByTestId('swap-details-header-row')))
expect(
screen.getByText(
'Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any of these fees.'
)
).toBeInTheDocument()
expect(screen.getByText(`${TEST_TOKEN_2.symbol} fee`)).toBeInTheDocument()
})
})
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
import {
TEST_ALLOWED_SLIPPAGE,
TEST_TOKEN_1,
TEST_TOKEN_2,
TEST_TRADE_EXACT_INPUT,
TEST_TRADE_EXACT_OUTPUT,
TEST_TRADE_FEE_ON_BUY,
TEST_TRADE_FEE_ON_SELL,
} from 'test-utils/constants'
import { render, screen, within } from 'test-utils/render'
import SwapModalFooter from './SwapModalFooter'
......@@ -97,4 +105,62 @@ describe('SwapModalFooter.tsx', () => {
).toBeInTheDocument()
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
})
it('test trade fee on input token transfer', () => {
render(
<SwapModalFooter
trade={TEST_TRADE_FEE_ON_SELL}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
fiatValueInput={{
data: undefined,
isLoading: false,
}}
fiatValueOutput={{
data: undefined,
isLoading: false,
}}
showAcceptChanges={true}
onAcceptChanges={jest.fn()}
/>
)
expect(
screen.getByText(
'Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any of these fees.'
)
).toBeInTheDocument()
expect(screen.getByText(`${TEST_TOKEN_1.symbol} fee`)).toBeInTheDocument()
})
it('test trade fee on output token transfer', () => {
render(
<SwapModalFooter
trade={TEST_TRADE_FEE_ON_BUY}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
fiatValueInput={{
data: undefined,
isLoading: false,
}}
fiatValueOutput={{
data: undefined,
isLoading: false,
}}
showAcceptChanges={true}
onAcceptChanges={jest.fn()}
/>
)
expect(
screen.getByText(
'Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any of these fees.'
)
).toBeInTheDocument()
expect(screen.getByText(`${TEST_TOKEN_2.symbol} fee`)).toBeInTheDocument()
})
})
import { Plural, Trans } from '@lingui/macro'
import { Plural, t, Trans } from '@lingui/macro'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { TraceEvent } from 'analytics'
import Column from 'components/Column'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { ZERO_PERCENT } from 'constants/misc'
import { SwapResult } from 'hooks/useSwapCallback'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import { InterfaceTrade, RouterPreference } from 'state/routing/types'
import { ClassicTrade, InterfaceTrade, RouterPreference } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils'
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components'
import styled, { DefaultTheme, useTheme } from 'styled-components'
import { ThemedText } from 'theme'
import { formatNumber, formatPriceImpact, NumberType } from 'utils/formatNumbers'
import { formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers'
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters'
import { getPriceImpactWarning } from 'utils/prices'
import { getPriceImpactColor } from 'utils/prices'
import { ButtonError, SmallButtonPrimary } from '../Button'
import Row, { AutoRow, RowBetween, RowFixed } from '../Row'
......@@ -41,9 +42,10 @@ const ConfirmButton = styled(ButtonError)`
margin-top: 10px;
`
const DetailRowValue = styled(ThemedText.BodySmall)`
const DetailRowValue = styled(ThemedText.BodySmall)<{ warningColor?: keyof DefaultTheme }>`
text-align: right;
overflow-wrap: break-word;
${({ warningColor, theme }) => warningColor && `color: ${theme[warningColor]};`};
`
export default function SwapModalFooter({
......@@ -112,18 +114,22 @@ export default function SwapModalFooter({
</Row>
</ThemedText.BodySmall>
{isClassicTrade(trade) && (
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<Label cursor="help">
<Trans>Price impact</Trans>
</Label>
</MouseoverTooltip>
<DetailRowValue color={getPriceImpactWarning(trade.priceImpact)}>
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
</DetailRowValue>
</Row>
</ThemedText.BodySmall>
<>
<TokenTaxLineItem trade={trade} type="input" />
<TokenTaxLineItem trade={trade} type="output" />
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<Label cursor="help">
<Trans>Price impact</Trans>
</Label>
</MouseoverTooltip>
<DetailRowValue warningColor={getPriceImpactColor(trade.priceImpact)}>
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
</DetailRowValue>
</Row>
</ThemedText.BodySmall>
</>
)}
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
......@@ -209,3 +215,28 @@ export default function SwapModalFooter({
</>
)
}
function TokenTaxLineItem({ trade, type }: { trade: ClassicTrade; type: 'input' | 'output' }) {
const [currency, percentage] =
type === 'input' ? [trade.inputAmount.currency, trade.inputTax] : [trade.outputAmount.currency, trade.outputTax]
if (percentage.equalTo(ZERO_PERCENT)) return null
return (
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip
text={
<Trans>
Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not
receive any of these fees.
</Trans>
}
>
<Label cursor="help">{t`${currency.symbol} fee`}</Label>
</MouseoverTooltip>
<DetailRowValue warningColor={getPriceImpactColor(percentage)}>{formatPriceImpact(percentage)}</DetailRowValue>
</Row>
</ThemedText.BodySmall>
)
}
......@@ -27,7 +27,7 @@ export default function SwapModalHeader({
allowedSlippage: Percent
}) {
const fiatValueInput = useUSDPrice(trade.inputAmount)
const fiatValueOutput = useUSDPrice(trade.outputAmount)
const fiatValueOutput = useUSDPrice(trade.postTaxOutputAmount)
return (
<HeaderContainer gap="sm">
......@@ -42,7 +42,7 @@ export default function SwapModalHeader({
<SwapModalHeaderAmount
field={Field.OUTPUT}
label={<Trans>You receive</Trans>}
amount={trade.outputAmount}
amount={trade.postTaxOutputAmount}
currency={trade.outputAmount.currency}
usdAmount={fiatValueOutput.data}
tooltipText={
......
......@@ -141,7 +141,7 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
</div>
</div>
<div
class="c6 css-zhpkf8"
class="c2 c6 css-zhpkf8"
>
105566.373%
</div>
......
import { ChainId, Currency, Percent } from '@uniswap/sdk-core'
import { ZERO_PERCENT } from './misc'
interface TokenTaxMetadata {
buyFee?: Percent
sellFee?: Percent
}
const CHAIN_TOKEN_TAX_MAP: { [chainId in number]?: { [address in string]?: TokenTaxMetadata } } = {
[ChainId.MAINNET]: {
// BULLET
'0x8ef32a03784c8fd63bbf027251b9620865bd54b6': {
buyFee: new Percent(5, 100), // 5%
sellFee: new Percent(5, 100), // 5%
},
// X
'0xabec00542d141bddf58649bfe860c6449807237c': {
buyFee: new Percent(1, 100), // 1%
sellFee: new Percent(1, 100), // 1%
},
// HarryPotterObamaKnuckles9Inu
'0x2577944fd4b556a99cc5aa0f072e4b944aa088df': {
buyFee: new Percent(1, 100), // 1%
sellFee: new Percent(11, 1000), // 1.1%
},
// QWN
'0xb354b5da5ea39dadb1cea8140bf242eb24b1821a': {
buyFee: new Percent(5, 100), // 5%
sellFee: new Percent(5, 100), // 5%
},
// HarryPotterObamaPacMan8Inu
'0x07e0edf8ce600fb51d44f51e3348d77d67f298ae': {
buyFee: new Percent(2, 100), // 2%
sellFee: new Percent(2, 100), // 2%
},
// KUKU
'0x27206f5a9afd0c51da95f20972885545d3b33647': {
buyFee: new Percent(2, 100), // 2%
sellFee: new Percent(21, 1000), // 2.1%
},
// AIMBOT
'0x0c48250eb1f29491f1efbeec0261eb556f0973c7': {
buyFee: new Percent(5, 100), // 5%
sellFee: new Percent(5, 100), // 5%
},
// PYUSD
'0xe0a8ed732658832fac18141aa5ad3542e2eb503b': {
buyFee: new Percent(1, 100), // 1%
sellFee: new Percent(13, 1000), // 1.3%
},
// ND4
'0x4f849c55180ddf8185c5cc495ed58c3aea9c9a28': {
buyFee: new Percent(1, 100), // 1%
sellFee: new Percent(1, 100), // 1%
},
// COCO
'0xcb50350ab555ed5d56265e096288536e8cac41eb': {
buyFee: new Percent(2, 100), // 2%
sellFee: new Percent(26, 1000), // 2.6%
},
},
}
export function getInputTax(currency: Currency): Percent {
if (currency.isNative) return ZERO_PERCENT
return CHAIN_TOKEN_TAX_MAP[currency.chainId]?.[currency.address.toLowerCase()]?.sellFee ?? ZERO_PERCENT
}
export function getOutputTax(currency: Currency): Percent {
if (currency.isNative) return ZERO_PERCENT
return CHAIN_TOKEN_TAX_MAP[currency.chainId]?.[currency.address.toLowerCase()]?.buyFee ?? ZERO_PERCENT
}
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFotAdjustmentsFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.fotAdjustedmentsEnabled)
}
export function useFotAdjustmentsEnabled(): boolean {
return useFotAdjustmentsFlag() === BaseVariant.Enabled
}
......@@ -16,6 +16,7 @@ export enum FeatureFlag {
uniswapXEthOutputEnabled = 'uniswapx_eth_output_enabled',
multichainUX = 'multichain_ux',
currencyConversion = 'currency_conversion',
fotAdjustedmentsEnabled = 'fot_adjustments_enabled',
}
interface FeatureFlagsContextType {
......
......@@ -67,13 +67,13 @@ export function useSwapCallback(
? {
tradeType: TradeType.EXACT_INPUT,
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.postTaxOutputAmount.quotient.toString(),
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
}
: {
tradeType: TradeType.EXACT_OUTPUT,
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
outputCurrencyAmountRaw: trade.postTaxOutputAmount.quotient.toString(),
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}),
}
......
......@@ -64,8 +64,13 @@ export function useUniversalRouterSwapCallback(
if (chainId !== connectedChainId) throw new WrongChainError()
setTraceData('slippageTolerance', options.slippageTolerance.toFixed(2))
// universal-router-sdk reconstructs V2Trade objects, so rather than updating the trade amounts to account for tax, we adjust the slippage tolerance as a workaround
// TODO(WEB-2725): update universal-router-sdk to not reconstruct trades
const taxAdjustedSlippageTolerance = options.slippageTolerance.add(trade.totalTaxRate)
const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, {
slippageTolerance: options.slippageTolerance,
slippageTolerance: taxAdjustedSlippageTolerance,
deadlineOrPreviousBlockhash: options.deadline?.toString(),
inputTokenPermit: options.permit,
fee: options.feeOptions,
......
import { SkipToken, skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useForceUniswapXOn } from 'featureFlags/flags/forceUniswapXOn'
import { useFotAdjustmentsEnabled } from 'featureFlags/flags/fotAdjustments'
import { useUniswapXEnabled } from 'featureFlags/flags/uniswapx'
import { useUniswapXEthOutputEnabled } from 'featureFlags/flags/uniswapXEthOutput'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
......@@ -34,6 +35,7 @@ export function useRoutingAPIArguments({
const forceUniswapXOn = useForceUniswapXOn()
const userDisabledUniswapX = useUserDisabledUniswapX()
const uniswapXEthOutputEnabled = useUniswapXEthOutputEnabled()
const fotAdjustmentsEnabled = useFotAdjustmentsEnabled()
return useMemo(
() =>
......@@ -58,6 +60,7 @@ export function useRoutingAPIArguments({
forceUniswapXOn,
userDisabledUniswapX,
uniswapXEthOutputEnabled,
fotAdjustmentsEnabled,
},
[
account,
......@@ -71,6 +74,7 @@ export function useRoutingAPIArguments({
forceUniswapXOn,
userDisabledUniswapX,
uniswapXEthOutputEnabled,
fotAdjustmentsEnabled,
]
)
}
......@@ -294,7 +294,7 @@ export function Swap({
}
: {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.postTaxOutputAmount,
},
[independentField, parsedAmount, showWrap, trade]
)
......@@ -314,13 +314,17 @@ export function Swap({
)
const fiatValueTradeInput = useUSDPrice(trade?.inputAmount)
const fiatValueTradeOutput = useUSDPrice(trade?.outputAmount)
const stablecoinPriceImpact = useMemo(
const fiatValueTradeOutput = useUSDPrice(trade?.postTaxOutputAmount)
const preTaxFiatValueTradeOutput = useUSDPrice(trade?.outputAmount)
const [stablecoinPriceImpact, preTaxStablecoinPriceImpact] = useMemo(
() =>
routeIsSyncing || !isClassicTrade(trade)
? undefined
: computeFiatValuePriceImpact(fiatValueTradeInput.data, fiatValueTradeOutput.data),
[fiatValueTradeInput, fiatValueTradeOutput, routeIsSyncing, trade]
? [undefined, undefined]
: [
computeFiatValuePriceImpact(fiatValueTradeInput.data, fiatValueTradeOutput.data),
computeFiatValuePriceImpact(fiatValueTradeInput.data, preTaxFiatValueTradeOutput.data),
],
[fiatValueTradeInput, fiatValueTradeOutput, preTaxFiatValueTradeOutput, routeIsSyncing, trade]
)
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers(dispatch)
......@@ -417,7 +421,7 @@ export function Swap({
if (!swapCallback) {
return
}
if (stablecoinPriceImpact && !confirmPriceImpactWithoutFee(stablecoinPriceImpact)) {
if (preTaxStablecoinPriceImpact && !confirmPriceImpactWithoutFee(preTaxStablecoinPriceImpact)) {
return
}
setSwapState((currentState) => ({
......@@ -440,7 +444,7 @@ export function Swap({
swapResult: undefined,
}))
})
}, [swapCallback, stablecoinPriceImpact])
}, [swapCallback, preTaxStablecoinPriceImpact])
const handleOnWrap = useCallback(async () => {
if (!onWrap) return
......@@ -476,9 +480,9 @@ export function Swap({
}
const marketPriceImpact = trade?.priceImpact ? computeRealizedPriceImpact(trade) : undefined
const largerPriceImpact = largerPercentValue(marketPriceImpact, stablecoinPriceImpact)
const largerPriceImpact = largerPercentValue(marketPriceImpact, preTaxStablecoinPriceImpact)
return { priceImpactSeverity: warningSeverity(largerPriceImpact), largerPriceImpact }
}, [stablecoinPriceImpact, trade])
}, [preTaxStablecoinPriceImpact, trade])
const handleConfirmDismiss = useCallback(() => {
setSwapState((currentState) => ({ ...currentState, showConfirm: false }))
......
import { MixedRouteSDK, Protocol, Trade } from '@uniswap/router-sdk'
import { ChainId, Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { MixedRouteSDK, ONE, Protocol, Trade } from '@uniswap/router-sdk'
import { ChainId, Currency, CurrencyAmount, Fraction, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { DutchOrderInfo, DutchOrderInfoJSON, DutchOrderTrade as IDutchOrderTrade } from '@uniswap/uniswapx-sdk'
import { Route as V2Route } from '@uniswap/v2-sdk'
import { Route as V3Route } from '@uniswap/v3-sdk'
......@@ -47,6 +47,7 @@ export interface GetQuoteArgs {
uniswapXEthOutputEnabled: boolean
forceUniswapXOn: boolean
userDisabledUniswapX: boolean
fotAdjustmentsEnabled: boolean
}
// from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts
......@@ -146,6 +147,8 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
isUniswapXBetter: boolean | undefined
requestId: string | undefined
quoteMethod: QuoteMethod
inputTax: Percent
outputTax: Percent
constructor({
gasUseEstimateUSD,
......@@ -154,6 +157,8 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
requestId,
quoteMethod,
approveInfo,
inputTax,
outputTax,
...routes
}: {
gasUseEstimateUSD?: number
......@@ -163,6 +168,8 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
requestId?: string
quoteMethod: QuoteMethod
approveInfo: ApproveInfo
inputTax: Percent
outputTax: Percent
v2Routes: {
routev2: V2Route<Currency, Currency>
inputAmount: CurrencyAmount<Currency>
......@@ -187,6 +194,26 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
this.requestId = requestId
this.quoteMethod = quoteMethod
this.approveInfo = approveInfo
this.inputTax = inputTax
this.outputTax = outputTax
}
public get totalTaxRate(): Percent {
return this.inputTax.add(this.outputTax)
}
public get postTaxOutputAmount() {
// Ideally we should calculate the final output amount by ammending the inputAmount based on the input tax and then applying the output tax,
// but this isn't currently possible because V2Trade reconstructs the total inputAmount based on the swap routes
// TODO(WEB-2761): Amend V2Trade objects in the v2-sdk to have a separate field for post-input tax routes
return this.outputAmount.multiply(new Fraction(ONE).subtract(this.totalTaxRate))
}
public minimumAmountOut(slippageTolerance: Percent, amountOut = this.outputAmount): CurrencyAmount<Currency> {
// Since universal-router-sdk reconstructs V2Trade objects, overriding this method does not actually change the minimumAmountOut that gets submitted on-chain
// Our current workaround is to add tax rate to slippage tolerance before we submit the trade to universal-router-sdk in useUniversalRouter.ts
// So the purpose of this override is so the UI displays the same minimum amount out as what is submitted on-chain
return super.minimumAmountOut(slippageTolerance.add(this.totalTaxRate), amountOut)
}
// gas estimate for maybe approve + swap
......@@ -259,6 +286,11 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
return 0
}
/** For UniswapX, handling token taxes in the output amount is outsourced to quoters */
public get postTaxOutputAmount() {
return this.outputAmount
}
}
export type InterfaceTrade = ClassicTrade | DutchOrderTrade
......
......@@ -6,7 +6,9 @@ import { DutchOrderInfo, DutchOrderInfoJSON } from '@uniswap/uniswapx-sdk'
import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk'
import { asSupportedChain } from 'constants/chains'
import { ZERO_PERCENT } from 'constants/misc'
import { RPC_PROVIDERS } from 'constants/providers'
import { getInputTax, getOutputTax } from 'constants/tax'
import { isAvalanche, isBsc, isMatic, nativeOnChain } from 'constants/tokens'
import { toSlippagePercent } from 'utils/slippage'
......@@ -213,6 +215,9 @@ export async function transformRoutesToTrade(
const approveInfo = await getApproveInfo(account, currencyIn, amount, usdCostPerGas)
const inputTax = args.fotAdjustmentsEnabled ? getInputTax(currencyIn) : ZERO_PERCENT
const outputTax = args.fotAdjustmentsEnabled ? getOutputTax(currencyOut) : ZERO_PERCENT
const classicTrade = new ClassicTrade({
v2Routes:
routes
......@@ -247,6 +252,8 @@ export async function transformRoutesToTrade(
isUniswapXBetter,
requestId: data.quote.requestId,
quoteMethod,
inputTax,
outputTax,
})
// During the opt-in period, only return UniswapX quotes if the user has turned on the setting,
......
import { ChainId, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { V3Route } from '@uniswap/smart-order-router'
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import { ZERO_PERCENT } from 'constants/misc'
import { nativeOnChain } from 'constants/tokens'
import { BigNumber } from 'ethers/lib/ethers'
import JSBI from 'jsbi'
......@@ -46,6 +47,8 @@ export const TEST_TRADE_EXACT_INPUT = new ClassicTrade({
gasUseEstimateUSD: 1.0,
approveInfo: { needsApprove: false },
quoteMethod: QuoteMethod.CLIENT_SIDE,
inputTax: ZERO_PERCENT,
outputTax: ZERO_PERCENT,
})
export const TEST_TRADE_EXACT_INPUT_API = new ClassicTrade({
......@@ -61,6 +64,8 @@ export const TEST_TRADE_EXACT_INPUT_API = new ClassicTrade({
gasUseEstimateUSD: 1.0,
approveInfo: { needsApprove: false },
quoteMethod: QuoteMethod.ROUTING_API,
inputTax: ZERO_PERCENT,
outputTax: ZERO_PERCENT,
})
export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({
......@@ -75,6 +80,8 @@ export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({
tradeType: TradeType.EXACT_OUTPUT,
quoteMethod: QuoteMethod.CLIENT_SIDE,
approveInfo: { needsApprove: false },
inputTax: ZERO_PERCENT,
outputTax: ZERO_PERCENT,
})
export const TEST_ALLOWED_SLIPPAGE = new Percent(2, 100)
......@@ -116,3 +123,37 @@ export const TEST_DUTCH_TRADE_ETH_INPUT = new DutchOrderTrade({
deadlineBufferSecs: 30,
slippageTolerance: new Percent(5, 100),
})
export const TEST_TRADE_FEE_ON_SELL = new ClassicTrade({
v3Routes: [
{
routev3: new V3Route([TEST_POOL_12], TEST_TOKEN_1, TEST_TOKEN_2),
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
outputAmount: toCurrencyAmount(TEST_TOKEN_2, 1000),
},
],
v2Routes: [],
tradeType: TradeType.EXACT_INPUT,
gasUseEstimateUSD: 1.0,
approveInfo: { needsApprove: false },
quoteMethod: QuoteMethod.ROUTING_API,
inputTax: new Percent(3, 100),
outputTax: ZERO_PERCENT,
})
export const TEST_TRADE_FEE_ON_BUY = new ClassicTrade({
v3Routes: [
{
routev3: new V3Route([TEST_POOL_12], TEST_TOKEN_1, TEST_TOKEN_2),
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
outputAmount: toCurrencyAmount(TEST_TOKEN_2, 1000),
},
],
v2Routes: [],
tradeType: TradeType.EXACT_INPUT,
gasUseEstimateUSD: 1.0,
approveInfo: { needsApprove: false },
quoteMethod: QuoteMethod.ROUTING_API,
inputTax: ZERO_PERCENT,
outputTax: new Percent(3, 100),
})
......@@ -3,6 +3,7 @@ import { Currency, CurrencyAmount, Fraction, Percent, TradeType } from '@uniswap
import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk'
import JSBI from 'jsbi'
import { DefaultTheme } from 'styled-components'
import {
ALLOWED_PRICE_IMPACT_HIGH,
......@@ -104,3 +105,14 @@ export function getPriceImpactWarning(priceImpact: Percent): 'warning' | 'error'
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 'warning'
return
}
export function getPriceImpactColor(priceImpact: Percent): keyof DefaultTheme | undefined {
switch (getPriceImpactWarning(priceImpact)) {
case 'error':
return 'accentFailure'
case 'warning':
return 'accentWarning'
default:
return undefined
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment