Commit 7ee761a5 authored by Moody Salem's avatar Moody Salem Committed by GitHub

feat: automatic slippage tolerance (#1463)

* automatic slippage tolerance start

* get it compiling

* out of range/in range behavior of slippage tolerance in add

* small useDerivedSwapInfo refactor

* improve useSwapSlippageTolerance

* fix unit test

* thread placeholder slippage through

* small improvement to slippage input behavior

* fix the display bug

* fix tx settings modal ux

* don't pass props unnecessarily

* switch back to static swap slippage for now

bump migrate slippage to .75%

* fix font size

* add flag for auto slippage migration

validate version updates even more
Co-authored-by: default avatarNoah Zinsmeister <noahwz@gmail.com>
parent 78e95f60
......@@ -3,6 +3,7 @@ import styled from 'styled-components'
import { darken } from 'polished'
import { useTranslation } from 'react-i18next'
import { NavLink, Link as HistoryLink } from 'react-router-dom'
import { Percent } from '@uniswap/sdk-core'
import { ArrowLeft } from 'react-feather'
import { RowBetween } from '../Row'
......@@ -80,7 +81,6 @@ export function FindPoolTabs({ origin }: { origin: string }) {
<StyledArrowLeft />
</HistoryLink>
<ActiveText>Import Pool</ActiveText>
<SettingsTab />
</RowBetween>
</Tabs>
)
......@@ -90,10 +90,12 @@ export function AddRemoveTabs({
adding,
creating,
positionID,
defaultSlippage,
}: {
adding: boolean
creating: boolean
positionID?: string | undefined
defaultSlippage: Percent
}) {
const theme = useTheme()
......@@ -118,7 +120,7 @@ export function AddRemoveTabs({
<TYPE.mediumHeader fontWeight={500} fontSize={20}>
{creating ? 'Create a pair' : adding ? 'Add Liquidity' : 'Remove Liquidity'}
</TYPE.mediumHeader>
<SettingsTab />
<SettingsTab placeholderSlippage={defaultSlippage} />
</RowBetween>
</Tabs>
)
......
import JSBI from 'jsbi'
import React, { useContext, useRef, useState } from 'react'
import { Settings, X } from 'react-feather'
import ReactGA from 'react-ga'
......@@ -7,12 +6,7 @@ import styled, { ThemeContext } from 'styled-components'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { ApplicationModal } from '../../state/application/actions'
import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks'
import {
useExpertModeManager,
useUserTransactionTTL,
useUserSlippageTolerance,
useUserSingleHopOnly,
} from '../../state/user/hooks'
import { useExpertModeManager, useUserSingleHopOnly } from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
......@@ -21,6 +15,7 @@ import QuestionHelper from '../QuestionHelper'
import { RowBetween, RowFixed } from '../Row'
import Toggle from '../Toggle'
import TransactionSettings from '../TransactionSettings'
import { Percent } from '@uniswap/sdk-core'
const StyledMenuIcon = styled(Settings)`
height: 20px;
......@@ -116,15 +111,12 @@ const ModalContentWrapper = styled.div`
border-radius: 20px;
`
export default function SettingsTab() {
export default function SettingsTab({ placeholderSlippage }: { placeholderSlippage: Percent }) {
const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.SETTINGS)
const toggle = useToggleSettingsMenu()
const theme = useContext(ThemeContext)
const [userSlippageTolerance, setUserslippageTolerance] = useUserSlippageTolerance()
const [ttl, setTtl] = useUserTransactionTTL()
const [expertMode, toggleExpertMode] = useExpertModeManager()
......@@ -191,12 +183,7 @@ export default function SettingsTab() {
<Text fontWeight={600} fontSize={14}>
Transaction Settings
</Text>
<TransactionSettings
rawSlippage={JSBI.toNumber(userSlippageTolerance.numerator)}
setRawSlippage={setUserslippageTolerance}
deadline={ttl}
setDeadline={setTtl}
/>
<TransactionSettings placeholderSlippage={placeholderSlippage} />
<Text fontWeight={600} fontSize={14}>
Interface Settings
</Text>
......
import React, { useState, useRef, useContext } from 'react'
import React, { useState, useContext } from 'react'
import { Percent } from '@uniswap/sdk-core'
import styled, { ThemeContext } from 'styled-components'
import QuestionHelper from '../QuestionHelper'
import { TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/index'
import { darken } from 'polished'
import { useSetUserSlippageTolerance, useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
enum SlippageError {
InvalidInput = 'InvalidInput',
RiskyLow = 'RiskyLow',
RiskyHigh = 'RiskyHigh',
}
enum DeadlineError {
......@@ -64,7 +64,8 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
position: relative;
padding: 0 0.75rem;
flex: 1;
border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`};
border: ${({ theme, active, warning }) =>
active ? `1px solid ${warning ? theme.red1 : theme.primary1}` : warning && `1px solid ${theme.red1}`};
:hover {
border: ${({ theme, active, warning }) =>
active && `1px solid ${warning ? darken(0.1, theme.red1) : darken(0.1, theme.primary1)}`};
......@@ -85,63 +86,63 @@ const SlippageEmojiContainer = styled.span`
`}
`
export interface SlippageTabsProps {
rawSlippage: number
setRawSlippage: (rawSlippage: number) => void
deadline: number
setDeadline: (deadline: number) => void
export interface TransactionSettingsProps {
placeholderSlippage: Percent // varies according to the context in which the settings dialog is placed
}
export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, setDeadline }: SlippageTabsProps) {
export default function TransactionSettings({ placeholderSlippage }: TransactionSettingsProps) {
const theme = useContext(ThemeContext)
const inputRef = useRef<HTMLInputElement>()
const userSlippageTolerance = useUserSlippageTolerance()
const setUserSlippageTolerance = useSetUserSlippageTolerance()
const [deadline, setDeadline] = useUserTransactionTTL()
const [slippageInput, setSlippageInput] = useState('')
const [slippageError, setSlippageError] = useState<SlippageError | false>(false)
const [deadlineInput, setDeadlineInput] = useState('')
const [deadlineError, setDeadlineError] = useState<DeadlineError | false>(false)
const slippageInputIsValid =
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
let slippageError: SlippageError | undefined
if (slippageInput !== '' && !slippageInputIsValid) {
slippageError = SlippageError.InvalidInput
} else if (slippageInputIsValid && rawSlippage < 5) {
slippageError = SlippageError.RiskyLow
} else if (slippageInputIsValid && rawSlippage > 100) {
slippageError = SlippageError.RiskyHigh
function parseSlippageInput(value: string) {
// populate what the user typed and clear the error
setSlippageInput(value)
setSlippageError(false)
if (value.length === 0) {
setUserSlippageTolerance('auto')
} else {
slippageError = undefined
}
const parsed = Math.floor(Number.parseFloat(value) * 100)
let deadlineError: DeadlineError | undefined
if (deadlineInput !== '' && !deadlineInputIsValid) {
deadlineError = DeadlineError.InvalidInput
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
setUserSlippageTolerance('auto')
if (value !== '.') {
setSlippageError(SlippageError.InvalidInput)
}
} else {
deadlineError = undefined
setUserSlippageTolerance(new Percent(parsed, 10_000))
}
function parseCustomSlippage(value: string) {
setSlippageInput(value)
try {
const valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(value) * 100).toString())
if (!Number.isNaN(valueAsIntFromRoundedFloat) && valueAsIntFromRoundedFloat < 5000) {
setRawSlippage(valueAsIntFromRoundedFloat)
}
} catch {}
}
const tooLow = userSlippageTolerance !== 'auto' && userSlippageTolerance.lessThan(new Percent(5, 10_000))
const tooHigh = userSlippageTolerance !== 'auto' && userSlippageTolerance.greaterThan(new Percent(1, 100))
function parseCustomDeadline(value: string) {
// populate what the user typed and clear the error
setDeadlineInput(value)
setDeadlineError(false)
try {
const valueAsInt: number = Number.parseInt(value) * 60
if (!Number.isNaN(valueAsInt) && valueAsInt > 0) {
setDeadline(valueAsInt)
if (value.length === 0) {
setDeadline(DEFAULT_DEADLINE_FROM_NOW)
} else {
const parsed: number = Math.floor(Number.parseFloat(value) * 60)
if (!Number.isInteger(parsed) || parsed < 60) {
setDeadlineError(DeadlineError.InvalidInput)
} else {
setDeadline(parsed)
}
}
} catch {}
}
return (
......@@ -156,71 +157,56 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<RowBetween>
<Option
onClick={() => {
setSlippageInput('')
setRawSlippage(10)
}}
active={rawSlippage === 10}
>
0.1%
</Option>
<Option
onClick={() => {
setSlippageInput('')
setRawSlippage(50)
parseSlippageInput('')
}}
active={rawSlippage === 50}
active={userSlippageTolerance === 'auto'}
>
0.5%
Auto
</Option>
<Option
onClick={() => {
setSlippageInput('')
setRawSlippage(100)
}}
active={rawSlippage === 100}
>
1%
</Option>
<OptionCustom active={![10, 50, 100].includes(rawSlippage)} warning={!slippageInputIsValid} tabIndex={-1}>
<OptionCustom active={userSlippageTolerance !== 'auto'} warning={!!slippageError} tabIndex={-1}>
<RowBetween>
{!!slippageInput &&
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
{tooLow || tooHigh ? (
<SlippageEmojiContainer>
<span role="img" aria-label="warning">
⚠️
</span>
</SlippageEmojiContainer>
) : null}
{/* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 */}
<Input
ref={inputRef as any}
placeholder={(rawSlippage / 100).toFixed(2)}
value={slippageInput}
placeholder={placeholderSlippage.toFixed(2)}
value={
slippageInput.length > 0
? slippageInput
: userSlippageTolerance === 'auto'
? ''
: userSlippageTolerance.toFixed(2)
}
onChange={(e) => parseSlippageInput(e.target.value)}
onBlur={() => {
parseCustomSlippage((rawSlippage / 100).toFixed(2))
setSlippageInput('')
setSlippageError(false)
}}
onChange={(e) => parseCustomSlippage(e.target.value)}
color={!slippageInputIsValid ? 'red' : ''}
color={slippageError ? 'red' : ''}
/>
%
</RowBetween>
</OptionCustom>
</RowBetween>
{!!slippageError && (
{slippageError || tooLow || tooHigh ? (
<RowBetween
style={{
fontSize: '14px',
paddingTop: '7px',
color: slippageError === SlippageError.InvalidInput ? 'red' : '#F3841E',
color: slippageError ? 'red' : '#F3841E',
}}
>
{slippageError === SlippageError.InvalidInput
{slippageError
? 'Enter a valid slippage percentage'
: slippageError === SlippageError.RiskyLow
: tooLow
? 'Your transaction may fail'
: 'Your transaction may be frontrun'}
</RowBetween>
)}
) : null}
</AutoColumn>
<AutoColumn gap="sm">
......@@ -231,15 +217,22 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<QuestionHelper text="Your transaction will revert if it is pending for more than this period of time." />
</RowFixed>
<RowFixed>
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
<OptionCustom style={{ width: '80px' }} warning={!!deadlineError} tabIndex={-1}>
<Input
color={!!deadlineError ? 'red' : undefined}
placeholder={(DEFAULT_DEADLINE_FROM_NOW / 60).toString()}
value={
deadlineInput.length > 0
? deadlineInput
: deadline === DEFAULT_DEADLINE_FROM_NOW
? ''
: (deadline / 60).toString()
}
onChange={(e) => parseCustomDeadline(e.target.value)}
onBlur={() => {
parseCustomDeadline((deadline / 60).toString())
setDeadlineInput('')
setDeadlineError(false)
}}
placeholder={(deadline / 60).toString()}
value={deadlineInput}
onChange={(e) => parseCustomDeadline(e.target.value)}
color={deadlineError ? 'red' : ''}
/>
</OptionCustom>
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
......
import { Percent } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import React, { useContext } from 'react'
import { ThemeContext } from 'styled-components'
import { useUserSlippageTolerance } from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { computePriceImpactWithMaximumSlippage } from '../../utils/computePriceImpactWithMaximumSlippage'
import { computeRealizedLPFeeAmount } from '../../utils/prices'
......@@ -13,13 +13,13 @@ import SwapRoute from './SwapRoute'
export interface AdvancedSwapDetailsProps {
trade?: V2Trade | V3Trade
allowedSlippage: Percent
}
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
export function AdvancedSwapDetails({ trade, allowedSlippage }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
const realizedLPFee = computeRealizedLPFeeAmount(trade)
const [allowedSlippage] = useUserSlippageTolerance()
return !trade ? null : (
<AutoColumn gap="8px">
......@@ -55,6 +55,17 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
<FormattedPriceImpact priceImpact={computePriceImpactWithMaximumSlippage(trade, allowedSlippage)} />
</TYPE.black>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
Slippage tolerance
</TYPE.black>
</RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
{allowedSlippage.toFixed(2)}%
</TYPE.black>
</RowBetween>
</AutoColumn>
)
}
import React from 'react'
import styled from 'styled-components'
import SettingsTab from '../Settings'
import { Percent } from '@uniswap/sdk-core'
import { RowBetween, RowFixed } from '../Row'
import { TYPE } from '../../theme'
......@@ -11,7 +12,7 @@ const StyledSwapHeader = styled.div`
color: ${({ theme }) => theme.text2};
`
export default function SwapHeader() {
export default function SwapHeader({ allowedSlippage }: { allowedSlippage: Percent }) {
return (
<StyledSwapHeader>
<RowBetween>
......@@ -21,9 +22,7 @@ export default function SwapHeader() {
</TYPE.black>
</RowFixed>
<RowFixed>
{/* <TradeInfo disabled={!trade} trade={trade} /> */}
{/* <div style={{ width: '8px' }}></div> */}
<SettingsTab />
<SettingsTab placeholderSlippage={allowedSlippage} />
</RowFixed>
</RowBetween>
</StyledSwapHeader>
......
......@@ -135,7 +135,7 @@ export default function SwapModalHeader({
</RowBetween>
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
<AdvancedSwapDetails trade={trade} />
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />
</LightCard>
{showAcceptChanges ? (
......
......@@ -25,7 +25,7 @@ export default memo(function SwapRoute({ trade }: { trade: V2Trade | V3Trade })
return (
<Fragment key={i}>
<Flex alignItems="end">
<TYPE.black fontSize={14} color={theme.text1} ml="0.145rem" mr="0.145rem">
<TYPE.black color={theme.text1} ml="0.145rem" mr="0.145rem">
{currency.symbol}
</TYPE.black>
</Flex>
......
......@@ -205,10 +205,8 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
export const NetworkContextName = 'NETWORK'
// default allowed slippage, in bips
export const INITIAL_ALLOWED_SLIPPAGE = new Percent(10, 10_000)
// 20 minutes, denominated in seconds
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 20
// 30 minutes, denominated in seconds
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30
// used for rewards deadlines
export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7)
......
......@@ -3,7 +3,6 @@ import { Router, Trade as V2Trade } from '@uniswap/v2-sdk'
import { SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
import { ChainId, Percent, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { INITIAL_ALLOWED_SLIPPAGE } from '../constants'
import { SWAP_ROUTER_ADDRESSES } from '../constants/v3'
import { getTradeVersion } from '../utils/getTradeVersion'
import { useTransactionAdder } from '../state/transactions/hooks'
......@@ -51,7 +50,7 @@ interface FailedCall extends SwapCallEstimate {
*/
function useSwapCallArguments(
trade: V2Trade | V3Trade | undefined, // trade to execute, required
allowedSlippage: Percent = INITIAL_ALLOWED_SLIPPAGE, // in bips
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined
): SwapCall[] {
......@@ -138,7 +137,7 @@ function useSwapCallArguments(
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
trade: V2Trade | V3Trade | undefined, // trade to execute, required
allowedSlippage: Percent = INITIAL_ALLOWED_SLIPPAGE, // in bips
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | undefined | null
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: string | null } {
......
import { Percent } from '@uniswap/sdk-core'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { useMemo } from 'react'
import { useUserSlippageToleranceWithDefault } from '../state/user/hooks'
const V2_SWAP_DEFAULT_SLIPPAGE = new Percent(45, 10_000) // .45%
const V3_SWAP_DEFAULT_SLIPPAGE = new Percent(25, 10_000) // .25%
const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10%
export default function useSwapSlippageTolerance(trade: V2Trade | V3Trade | undefined): Percent {
const defaultSlippageTolerance = useMemo(() => {
if (!trade) return ONE_TENTHS_PERCENT
if (trade instanceof V2Trade) return V2_SWAP_DEFAULT_SLIPPAGE
return V3_SWAP_DEFAULT_SLIPPAGE
}, [trade])
return useUserSlippageToleranceWithDefault(defaultSlippageTolerance)
}
import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, TokenAmount, ETHER, currencyEquals } from '@uniswap/sdk-core'
import { Currency, TokenAmount, ETHER, currencyEquals, Percent } from '@uniswap/sdk-core'
import { WETH9 } from '@uniswap/sdk-core'
import { AlertTriangle, AlertCircle } from 'react-feather'
import ReactGA from 'react-ga'
import { ZERO_PERCENT } from '../../constants'
import { useV3NFTPositionManagerContract } from '../../hooks/useContract'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
......@@ -25,7 +26,7 @@ import { useWalletModalToggle } from '../../state/application/hooks'
import { Field, Bound } from '../../state/mint/v3/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageTolerance } from '../../state/user/hooks'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { TYPE, ExternalLink } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import AppBody from '../AppBody'
......@@ -52,6 +53,8 @@ import { BigNumber } from '@ethersproject/bignumber'
import { calculateGasMargin } from 'utils'
import { AddRemoveTabs } from 'components/NavigationTabs'
const DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE = new Percent(50, 10_000)
export default function AddLiquidity({
match: {
params: { currencyIdA, currencyIdB, feeAmount: feeAmountFromUrl, tokenId },
......@@ -148,7 +151,7 @@ export default function AddLiquidity({
// txn values
const deadline = useTransactionDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const [txHash, setTxHash] = useState<string>('')
// get formatted amounts
......@@ -193,6 +196,10 @@ export default function AddLiquidity({
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
)
const allowedSlippage = useUserSlippageToleranceWithDefault(
outOfRange ? ZERO_PERCENT : DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE
)
async function onAdd() {
if (!chainId || !library || !account) return
......@@ -396,7 +403,12 @@ export default function AddLiquidity({
pendingText={pendingText}
/>
<AppBody>
<AddRemoveTabs creating={false} adding={true} positionID={tokenId} />
<AddRemoveTabs
creating={false}
adding={true}
positionID={tokenId}
defaultSlippage={DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE}
/>
<Wrapper>
<AutoColumn gap="32px">
{!hasExistingPosition && (
......
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, currencyEquals, ETHER, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import { Currency, currencyEquals, ETHER, Percent, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import React, { useCallback, useContext, useState } from 'react'
import { Plus } from 'react-feather'
import ReactGA from 'react-ga'
......@@ -30,7 +30,7 @@ import { Field } from '../../state/mint/actions'
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageTolerance } from '../../state/user/hooks'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount } from '../../utils'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
......@@ -42,6 +42,8 @@ import { currencyId } from '../../utils/currencyId'
import { PoolPriceBar } from './PoolPriceBar'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
const DEFAULT_ADD_V2_SLIPPAGE_TOLERANCE = new Percent(50, 10_000)
export default function AddLiquidity({
match: {
params: { currencyIdA, currencyIdB },
......@@ -90,7 +92,7 @@ export default function AddLiquidity({
// txn values
const deadline = useTransactionDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_ADD_V2_SLIPPAGE_TOLERANCE) // custom from users
const [txHash, setTxHash] = useState<string>('')
// get formatted amounts
......@@ -315,7 +317,7 @@ export default function AddLiquidity({
return (
<>
<AppBody>
<AddRemoveTabs creating={isCreate} adding={true} />
<AddRemoveTabs creating={isCreate} adding={true} defaultSlippage={DEFAULT_ADD_V2_SLIPPAGE_TOLERANCE} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
......
import React, { useCallback, useMemo, useState, useEffect } from 'react'
import { Fraction, Price, Token, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import { Fraction, Percent, Price, Token, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import { FACTORY_ADDRESS, JSBI } from '@uniswap/v2-sdk'
import { Redirect, RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
......@@ -25,7 +25,7 @@ import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback'
import { Dots } from 'components/swap/styleds'
import { ButtonConfirmed } from 'components/Button'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import { useUserSlippageTolerance } from 'state/user/hooks'
import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'
import ReactGA from 'react-ga'
import { TransactionResponse } from '@ethersproject/providers'
import { useIsTransactionPending, useTransactionAdder } from 'state/transactions/hooks'
......@@ -49,6 +49,8 @@ import SettingsTab from 'components/Settings'
const ZERO = JSBI.BigInt(0)
const DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE = new Percent(75, 10_000)
function EmptyState({ message }: { message: string }) {
return (
<AutoColumn style={{ minHeight: 200, justifyContent: 'center', alignItems: 'center' }}>
......@@ -119,7 +121,7 @@ function V2PairMigration({
const deadline = useTransactionDeadline() // custom from users settings
const blockTimestamp = useCurrentBlockTimestamp()
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE) // custom from users
const currency0 = unwrappedToken(token0)
const currency1 = unwrappedToken(token1)
......@@ -673,7 +675,7 @@ export default function MigrateV2Pair({
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<BackArrow to="/migrate/v2" />
<TYPE.mediumHeader>Migrate V2 Liquidity</TYPE.mediumHeader>
<SettingsTab />
<SettingsTab placeholderSlippage={DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE} />
</AutoRow>
{!account ? (
......
......@@ -15,13 +15,13 @@ import { Text } from 'rebass'
import CurrencyLogo from 'components/CurrencyLogo'
import FormattedCurrencyAmount from 'components/FormattedCurrencyAmount'
import { useV3NFTPositionManagerContract } from 'hooks/useContract'
import { useUserSlippageTolerance } from 'state/user/hooks'
import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import ReactGA from 'react-ga'
import { useActiveWeb3React } from 'hooks'
import { TransactionResponse } from '@ethersproject/providers'
import { useTransactionAdder } from 'state/transactions/hooks'
import { WETH9, CurrencyAmount } from '@uniswap/sdk-core'
import { WETH9, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { TYPE } from 'theme'
import { Wrapper, SmallMaxButton, ResponsiveHeaderText } from './styled'
import Loader from 'components/Loader'
......@@ -37,6 +37,8 @@ import RangeBadge from 'components/Badge/RangeBadge'
export const UINT128MAX = BigNumber.from(2).pow(128).sub(1)
const DEFAULT_REMOVE_V3_LIQUIDITY_SLIPPAGE_TOLERANCE = new Percent(5, 100)
// redirect invalid tokenIds
export default function RemoveLiquidityV3({
location,
......@@ -89,7 +91,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
const [percentForSlider, onPercentSelectForSlider] = useDebouncedChangeHandler(percent, onPercentSelect)
const deadline = useTransactionDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_REMOVE_V3_LIQUIDITY_SLIPPAGE_TOLERANCE) // custom from users
const [showConfirm, setShowConfirm] = useState(false)
const [attemptingTxn, setAttemptingTxn] = useState(false)
......@@ -274,7 +276,12 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
pendingText={pendingText}
/>
<AppBody>
<AddRemoveTabs creating={false} adding={false} positionID={tokenId.toString()} />
<AddRemoveTabs
creating={false}
adding={false}
positionID={tokenId.toString()}
defaultSlippage={DEFAULT_REMOVE_V3_LIQUIDITY_SLIPPAGE_TOLERANCE}
/>
<Wrapper>
{position ? (
<AutoColumn gap="lg">
......
......@@ -40,9 +40,11 @@ import { useBurnActionHandlers } from '../../state/burn/hooks'
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useUserSlippageTolerance } from '../../state/user/hooks'
import { useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { BigNumber } from '@ethersproject/bignumber'
const DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE = new Percent(5, 100)
export default function RemoveLiquidity({
history,
match: {
......@@ -76,7 +78,7 @@ export default function RemoveLiquidity({
// txn values
const [txHash, setTxHash] = useState<string>('')
const deadline = useTransactionDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE)
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
......@@ -426,7 +428,7 @@ export default function RemoveLiquidity({
return (
<>
<AppBody>
<AddRemoveTabs creating={false} adding={false} />
<AddRemoveTabs creating={false} adding={false} defaultSlippage={DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
......
......@@ -46,7 +46,7 @@ import {
useSwapActionHandlers,
useSwapState,
} from '../../state/swap/hooks'
import { useExpertModeManager, useUserSingleHopOnly, useUserSlippageTolerance } from '../../state/user/hooks'
import { useExpertModeManager, useUserSingleHopOnly } from '../../state/user/hooks'
import { HideSmall, LinkStyledButton, TYPE } from '../../theme'
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
import { computePriceImpactWithMaximumSlippage } from '../../utils/computePriceImpactWithMaximumSlippage'
......@@ -100,19 +100,21 @@ export default function Swap({ history }: RouteComponentProps) {
// for expert mode
const [isExpertMode] = useExpertModeManager()
// get custom setting values for user
const [allowedSlippage] = useUserSlippageTolerance()
// get version from the url
const toggledVersion = useToggledVersion()
// swap state
const { independentField, typedValue, recipient } = useSwapState()
const {
v2Trade,
v3TradeState: { trade: v3Trade, state: v3TradeState },
toggledTrade: trade,
allowedSlippage,
currencyBalances,
parsedAmount,
currencies,
inputError: swapInputError,
} = useDerivedSwapInfo()
} = useDerivedSwapInfo(toggledVersion)
const { wrapType, execute: onWrap, inputError: wrapInputError } = useWrapCallback(
currencies[Field.INPUT],
......@@ -121,13 +123,6 @@ export default function Swap({ history }: RouteComponentProps) {
)
const showWrap: boolean = wrapType !== WrapType.NOT_APPLICABLE
const { address: recipientAddress } = useENSAddress(recipient)
const toggledVersion = useToggledVersion()
const trade = showWrap
? undefined
: {
[Version.v2]: v2Trade,
[Version.v3]: v3Trade ?? undefined,
}[toggledVersion]
const parsedAmounts = useMemo(
() =>
......@@ -357,7 +352,7 @@ export default function Swap({ history }: RouteComponentProps) {
onDismiss={handleDismissTokenWarning}
/>
<AppBody>
<SwapHeader />
<SwapHeader allowedSlippage={allowedSlippage} />
<Wrapper id="swap-page">
<ConfirmSwapModal
isOpen={showConfirm}
......@@ -484,7 +479,9 @@ export default function Swap({ history }: RouteComponentProps) {
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
<MouseoverTooltipContent content={<AdvancedSwapDetails trade={trade} />}>
<MouseoverTooltipContent
content={<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />}
>
<StyledInfo />
</MouseoverTooltipContent>
</RowFixed>
......
......@@ -2,13 +2,15 @@ import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useBestV3TradeExactIn, useBestV3TradeExactOut, V3TradeState } from '../../hooks/useBestV3Trade'
import useENS from '../../hooks/useENS'
import { parseUnits } from '@ethersproject/units'
import { Currency, CurrencyAmount, ETHER, Token, TokenAmount } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, ETHER, Token, TokenAmount, Percent } from '@uniswap/sdk-core'
import { JSBI, Trade as V2Trade } from '@uniswap/v2-sdk'
import { ParsedQs } from 'qs'
import { useCallback, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens'
import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance'
import { Version } from '../../hooks/useToggledVersion'
import { useV2TradeExactIn, useV2TradeExactOut } from '../../hooks/useV2Trade'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { isAddress } from '../../utils'
......@@ -16,7 +18,6 @@ import { AppDispatch, AppState } from '../index'
import { useCurrencyBalances } from '../wallet/hooks'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'
import { SwapState } from './reducer'
import { useUserSlippageTolerance } from '../user/hooks'
export function useSwapState(): AppState['swap'] {
return useSelector<AppState, AppState['swap']>((state) => state.swap)
......@@ -109,13 +110,17 @@ function involvesAddress(trade: V2Trade | V3Trade, checksummedAddress: string):
}
// from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(): {
export function useDerivedSwapInfo(
toggledVersion: Version
): {
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount }
parsedAmount: CurrencyAmount | undefined
v2Trade: V2Trade | undefined
inputError?: string
v2Trade: V2Trade | undefined
v3TradeState: { trade: V3Trade | null; state: V3TradeState }
toggledTrade: V2Trade | V3Trade | undefined
allowedSlippage: Percent
} {
const { account } = useActiveWeb3React()
......@@ -185,7 +190,8 @@ export function useDerivedSwapInfo(): {
}
}
const [allowedSlippage] = useUserSlippageTolerance()
const toggledTrade = (toggledVersion === Version.v2 ? v2Trade : v3Trade.trade) ?? undefined
const allowedSlippage = useSwapSlippageTolerance(toggledTrade)
// compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], v2Trade?.maximumAmountIn(allowedSlippage)]
......@@ -198,9 +204,11 @@ export function useDerivedSwapInfo(): {
currencies,
currencyBalances,
parsedAmount,
v2Trade: v2Trade ?? undefined,
inputError,
v2Trade: v2Trade ?? undefined,
v3TradeState: v3Trade,
toggledTrade,
allowedSlippage,
}
}
......
......@@ -17,7 +17,7 @@ export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>(
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
export const updateUserSingleHopOnly = createAction<{ userSingleHopOnly: boolean }>('user/updateUserSingleHopOnly')
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>(
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number | 'auto' }>(
'user/updateUserSlippageTolerance'
)
export const updateUserDeadline = createAction<{ userDeadline: number }>('user/updateUserDeadline')
......
import { ChainId, Percent, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import JSBI from 'jsbi'
import flatMap from 'lodash.flatmap'
import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
......@@ -14,12 +15,12 @@ import {
removeSerializedToken,
SerializedPair,
SerializedToken,
toggleURLWarning,
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
updateUserSlippageTolerance,
toggleURLWarning,
updateUserSingleHopOnly,
updateUserSlippageTolerance,
} from './actions'
function serializeToken(token: Token): SerializedToken {
......@@ -100,22 +101,51 @@ export function useUserSingleHopOnly(): [boolean, (newSingleHopOnly: boolean) =>
return [singleHopOnly, setSingleHopOnly]
}
export function useUserSlippageTolerance(): [Percent, (slippageBips: number) => void] {
export function useSetUserSlippageTolerance(): (slippageTolerance: Percent | 'auto') => void {
const dispatch = useDispatch<AppDispatch>()
const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>((state) => {
return state.user.userSlippageTolerance
})
const percentage = useMemo(() => new Percent(userSlippageTolerance, 10_000), [userSlippageTolerance])
const setUserSlippageTolerance = useCallback(
(userSlippageTolerance: number) => {
dispatch(updateUserSlippageTolerance({ userSlippageTolerance }))
return useCallback(
(userSlippageTolerance: Percent | 'auto') => {
let value: 'auto' | number
try {
value =
userSlippageTolerance === 'auto' ? 'auto' : JSBI.toNumber(userSlippageTolerance.multiply(10_000).quotient)
} catch (error) {
value = 'auto'
}
dispatch(
updateUserSlippageTolerance({
userSlippageTolerance: value,
})
)
},
[dispatch]
)
}
return [percentage, setUserSlippageTolerance]
/**
* Return the user's slippage tolerance, from the redux store, and a function to update the slippage tolerance
*/
export function useUserSlippageTolerance(): Percent | 'auto' {
const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>((state) => {
return state.user.userSlippageTolerance
})
return useMemo(() => (userSlippageTolerance === 'auto' ? 'auto' : new Percent(userSlippageTolerance, 10_000)), [
userSlippageTolerance,
])
}
/**
* Same as above but replaces the auto with a default value
* @param defaultSlippageTolerance the default value to replace auto with
*/
export function useUserSlippageToleranceWithDefault(defaultSlippageTolerance: Percent): Percent {
const allowedSlippage = useUserSlippageTolerance()
return useMemo(() => (allowedSlippage === 'auto' ? defaultSlippageTolerance : allowedSlippage), [
allowedSlippage,
defaultSlippageTolerance,
])
}
export function useUserTransactionTTL(): [number, (slippage: number) => void] {
......
......@@ -27,7 +27,7 @@ describe('swap reducer', () => {
} as any)
store.dispatch(updateVersion())
expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
expect(store.getState().userSlippageTolerance).toEqual(10)
expect(store.getState().userSlippageTolerance).toEqual('auto')
})
})
})
......@@ -31,7 +31,8 @@ export interface UserState {
userSingleHopOnly: boolean // only allow swaps on direct pairs
// user defined slippage tolerance in bips, used in all txns
userSlippageTolerance: number
userSlippageTolerance: number | 'auto'
userSlippageToleranceHasBeenMigratedToAuto: boolean // temporary flag for migration status
// deadline set by user in minutes, used in all txns
userDeadline: number
......@@ -62,7 +63,8 @@ export const initialState: UserState = {
matchesDarkMode: false,
userExpertMode: false,
userSingleHopOnly: false,
userSlippageTolerance: 10,
userSlippageTolerance: 'auto',
userSlippageToleranceHasBeenMigratedToAuto: true,
userDeadline: DEFAULT_DEADLINE_FROM_NOW,
tokens: {},
pairs: {},
......@@ -75,13 +77,26 @@ export default createReducer(initialState, (builder) =>
.addCase(updateVersion, (state) => {
// slippage isnt being tracked in local storage, reset to default
// noinspection SuspiciousTypeOfGuard
if (typeof state.userSlippageTolerance !== 'number') {
state.userSlippageTolerance = 10
if (
typeof state.userSlippageTolerance !== 'number' ||
!Number.isInteger(state.userSlippageTolerance) ||
state.userSlippageTolerance < 0 ||
state.userSlippageTolerance > 5000
) {
state.userSlippageTolerance = 'auto'
} else {
if (
!state.userSlippageToleranceHasBeenMigratedToAuto &&
[10, 50, 100].indexOf(state.userSlippageTolerance) !== -1
) {
state.userSlippageTolerance = 'auto'
state.userSlippageToleranceHasBeenMigratedToAuto = true
}
}
// deadline isnt being tracked in local storage, reset to default
// noinspection SuspiciousTypeOfGuard
if (typeof state.userDeadline !== 'number') {
if (typeof state.userDeadline !== 'number' || !Number.isInteger(state.userDeadline) || state.userDeadline < 60) {
state.userDeadline = DEFAULT_DEADLINE_FROM_NOW
}
......
......@@ -13,46 +13,48 @@ const THIRTY_BIPS_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000))
const ONE_HUNDRED_PERCENT = new Percent(JSBI.BigInt(10000), JSBI.BigInt(10000))
const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(THIRTY_BIPS_FEE)
// computes price breakdown for the trade
export function computeRealizedLPFeeAmount(trade?: V2Trade | V3Trade | null): CurrencyAmount | undefined {
// computes realized lp fee as a percent
export function computeRealizedLPFeePercent(trade: V2Trade | V3Trade): Percent {
let percent: Percent
if (trade instanceof V2Trade) {
// for each hop in our trade, take away the x*y=k price impact from 0.3% fees
// e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
const realizedLPFee = !trade
? undefined
: ONE_HUNDRED_PERCENT.subtract(
percent = ONE_HUNDRED_PERCENT.subtract(
trade.route.pairs.reduce<Fraction>(
(currentFee: Fraction): Fraction => currentFee.multiply(INPUT_FRACTION_AFTER_FEE),
ONE_HUNDRED_PERCENT
)
)
// the amount of the input that accrues to LPs
return (
realizedLPFee &&
trade &&
(trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient))
)
} else if (trade instanceof V3Trade) {
const realizedLPFee = !trade
? undefined
: ONE_HUNDRED_PERCENT.subtract(
} else {
percent = ONE_HUNDRED_PERCENT.subtract(
trade.route.pools.reduce<Fraction>(
(currentFee: Fraction, pool): Fraction =>
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))),
ONE_HUNDRED_PERCENT
)
)
return (
realizedLPFee &&
trade &&
(trade.inputAmount instanceof TokenAmount
}
return new Percent(percent.numerator, percent.denominator)
}
// computes price breakdown for the trade
export function computeRealizedLPFeeAmount(trade?: V2Trade | V3Trade | null): CurrencyAmount | undefined {
if (trade instanceof V2Trade) {
const realizedLPFee = computeRealizedLPFeePercent(trade)
// the amount of the input that accrues to LPs
return trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient))
)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient)
} else if (trade instanceof V3Trade) {
const realizedLPFee = computeRealizedLPFeePercent(trade)
return trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient)
}
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