Commit 828f7ee4 authored by Moody Salem's avatar Moody Salem Committed by GitHub

Use permit in swap (#65)

* add a usePermit hook

* semi working

* Fix code style issues with ESLint

* don't override gas, some cleanup in the permit function

* fix permit validity

* some more permit fixes

* nits

* another nit

* use the erc20 permit hook everywhere

* unused exports

* handle missing version

* replace everywhere

* add DAI and todos

* woopsie bug
Co-authored-by: default avatarLint Action <lint-action@samuelmeuli.com>
parent 432d17bd
[
{
"constant": true,
"inputs": [{ "name": "owner", "type": "address" }],
"name": "nonces",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "DOMAIN_SEPARATOR",
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
import React, { useState, useCallback } from 'react' import React, { useState, useCallback } from 'react'
import useIsArgentWallet from '../../hooks/useIsArgentWallet' import { V2_ROUTER_ADDRESS } from '../../constants'
import { useV2LiquidityTokenPermit } from '../../hooks/useERC20Permit'
import useTransactionDeadline from '../../hooks/useTransactionDeadline' import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import Modal from '../Modal' import Modal from '../Modal'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
...@@ -15,7 +16,6 @@ import { useActiveWeb3React } from '../../hooks' ...@@ -15,7 +16,6 @@ import { useActiveWeb3React } from '../../hooks'
import { maxAmountSpend } from '../../utils/maxAmountSpend' import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { usePairContract, useStakingContract } from '../../hooks/useContract' import { usePairContract, useStakingContract } from '../../hooks/useContract'
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback' import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
import { splitSignature } from 'ethers/lib/utils'
import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks' import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks'
import { wrappedCurrencyAmount } from '../../utils/wrappedCurrency' import { wrappedCurrencyAmount } from '../../utils/wrappedCurrency'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
...@@ -44,7 +44,7 @@ interface StakingModalProps { ...@@ -44,7 +44,7 @@ interface StakingModalProps {
} }
export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiquidityUnstaked }: StakingModalProps) { export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiquidityUnstaked }: StakingModalProps) {
const { account, chainId, library } = useActiveWeb3React() const { chainId, library } = useActiveWeb3React()
// track and parse user input // track and parse user input
const [typedValue, setTypedValue] = useState('') const [typedValue, setTypedValue] = useState('')
...@@ -76,10 +76,9 @@ export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiqui ...@@ -76,10 +76,9 @@ export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiqui
// approval data for stake // approval data for stake
const deadline = useTransactionDeadline() const deadline = useTransactionDeadline()
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null) const { signatureData, gatherPermitSignature } = useV2LiquidityTokenPermit(parsedAmountWrapped, V2_ROUTER_ADDRESS)
const [approval, approveCallback] = useApproveCallback(parsedAmount, stakingInfo.stakingRewardAddress) const [approval, approveCallback] = useApproveCallback(parsedAmount, stakingInfo.stakingRewardAddress)
const isArgentWallet = useIsArgentWallet()
const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress) const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress)
async function onStake() { async function onStake() {
setAttempting(true) setAttempting(true)
...@@ -115,7 +114,6 @@ export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiqui ...@@ -115,7 +114,6 @@ export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiqui
// wrapped onUserInput to clear signatures // wrapped onUserInput to clear signatures
const onUserInput = useCallback((typedValue: string) => { const onUserInput = useCallback((typedValue: string) => {
setSignatureData(null)
setTypedValue(typedValue) setTypedValue(typedValue)
}, []) }, [])
...@@ -128,69 +126,9 @@ export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiqui ...@@ -128,69 +126,9 @@ export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiqui
async function onAttemptToApprove() { async function onAttemptToApprove() {
if (!pairContract || !library || !deadline) throw new Error('missing dependencies') if (!pairContract || !library || !deadline) throw new Error('missing dependencies')
const liquidityAmount = parsedAmount if (!parsedAmount) throw new Error('missing liquidity amount')
if (!liquidityAmount) throw new Error('missing liquidity amount')
if (isArgentWallet) { return gatherPermitSignature ? gatherPermitSignature() : approveCallback()
return approveCallback()
}
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Uniswap V2',
version: '1',
chainId: chainId,
verifyingContract: pairContract.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: stakingInfo.stakingRewardAddress,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadline.toNumber(),
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadline.toNumber(),
})
})
.catch((error) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
} }
return ( return (
......
...@@ -23,7 +23,12 @@ const StyledPriceContainer = styled.div` ...@@ -23,7 +23,12 @@ const StyledPriceContainer = styled.div`
export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) { export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const formattedPrice = showInverted ? price.toSignificant(6) : price.invert()?.toSignificant(6) let formattedPrice: string
try {
formattedPrice = showInverted ? price.toSignificant(6) : price.invert()?.toSignificant(6)
} catch (error) {
formattedPrice = '0'
}
const label = showInverted ? `${price.quoteCurrency?.symbol}` : `${price.baseCurrency?.symbol} ` const label = showInverted ? `${price.quoteCurrency?.symbol}` : `${price.baseCurrency?.symbol} `
const labelInverted = showInverted ? `${price.baseCurrency?.symbol} ` : `${price.quoteCurrency?.symbol}` const labelInverted = showInverted ? `${price.baseCurrency?.symbol} ` : `${price.quoteCurrency?.symbol}`
......
...@@ -22,6 +22,7 @@ import MULTICALL_ABI from 'abis/multicall2.json' ...@@ -22,6 +22,7 @@ import MULTICALL_ABI from 'abis/multicall2.json'
import { Unisocks } from 'abis/types/Unisocks' import { Unisocks } from 'abis/types/Unisocks'
import UNISOCKS_ABI from 'abis/unisocks.json' import UNISOCKS_ABI from 'abis/unisocks.json'
import WETH_ABI from 'abis/weth.json' import WETH_ABI from 'abis/weth.json'
import EIP_2612 from 'abis/eip_2612.json'
import { import {
ARGENT_WALLET_DETECTOR_MAINNET_ADDRESS, ARGENT_WALLET_DETECTOR_MAINNET_ADDRESS,
...@@ -115,6 +116,10 @@ export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossi ...@@ -115,6 +116,10 @@ export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossi
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible) return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
} }
export function useEIP2612Contract(tokenAddress?: string): Contract | null {
return useContract(tokenAddress, EIP_2612, false)
}
export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null { export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible) return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
} }
......
import JSBI from 'jsbi'
import { ChainId, CurrencyAmount, Percent, TokenAmount } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { splitSignature } from 'ethers/lib/utils'
import { useMemo, useState } from 'react'
import { UNI, USDC, DAI } from '../constants'
import { SWAP_ROUTER_ADDRESSES } from '../constants/v3'
import { useSingleCallResult } from '../state/multicall/hooks'
import { useActiveWeb3React } from './index'
import { useEIP2612Contract } from './useContract'
import useIsArgentWallet from './useIsArgentWallet'
import useTransactionDeadline from './useTransactionDeadline'
enum PermitType {
AMOUNT = 1,
ALLOWED = 2,
}
// 20 minutes to submit after signing
const PERMIT_VALIDITY_BUFFER = 20 * 60
interface PermitInfo {
type: PermitType
name: string
// version is optional, and if omitted, will not be included in the domain
version?: string
}
// todo: read this information from extensions on token lists
const PERMITTABLE_TOKENS: {
[chainId in ChainId]: {
[checksummedTokenAddress: string]: PermitInfo
}
} = {
[ChainId.MAINNET]: {
[USDC.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '1' },
[DAI.address]: { type: PermitType.ALLOWED, name: 'Dai Stablecoin', version: '1' },
[UNI[ChainId.MAINNET].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
},
[ChainId.RINKEBY]: {
[UNI[ChainId.RINKEBY].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
},
[ChainId.ROPSTEN]: {
[UNI[ChainId.ROPSTEN].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
},
[ChainId.GÖRLI]: {
[UNI[ChainId.GÖRLI].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
},
[ChainId.KOVAN]: {
[UNI[ChainId.KOVAN].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
},
}
export enum UseERC20PermitState {
// returned for any reason, e.g. it is an argent wallet, or the currency does not support it
NOT_APPLICABLE,
LOADING,
NOT_SIGNED,
SIGNED,
}
export interface SignatureData {
v: number
r: string
s: string
deadline: number
nonce: number
amount: string
owner: string
spender: string
chainId: ChainId | number
tokenAddress: string
}
const EIP712_DOMAIN_TYPE = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const EIP712_DOMAIN_TYPE_NO_VERSION = [
{ name: 'name', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const EIP2612_TYPE = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
export function useERC20Permit(
currencyAmount: CurrencyAmount | null | undefined,
spender: string | null | undefined,
overridePermitInfo: PermitInfo | undefined | null
): {
signatureData: SignatureData | null
state: UseERC20PermitState
gatherPermitSignature: null | (() => Promise<void>)
} {
const { account, chainId, library } = useActiveWeb3React()
const transactionDeadline = useTransactionDeadline()
const tokenAddress = currencyAmount instanceof TokenAmount ? currencyAmount.token.address : undefined
const eip2612Contract = useEIP2612Contract(tokenAddress)
const isArgentWallet = useIsArgentWallet()
const nonceInputs = useMemo(() => [account ?? undefined], [account])
const tokenNonceState = useSingleCallResult(eip2612Contract, 'nonces', nonceInputs)
const permitInfo =
overridePermitInfo ?? (chainId && tokenAddress ? PERMITTABLE_TOKENS[chainId][tokenAddress] : undefined)
const [signatureData, setSignatureData] = useState<SignatureData | null>(null)
return useMemo(() => {
if (
isArgentWallet ||
!currencyAmount ||
!eip2612Contract ||
!account ||
!chainId ||
!transactionDeadline ||
!library ||
!tokenNonceState.valid ||
!tokenAddress ||
!spender ||
!permitInfo ||
// todo: support allowed permit
permitInfo.type !== PermitType.AMOUNT
) {
return {
state: UseERC20PermitState.NOT_APPLICABLE,
signatureData: null,
gatherPermitSignature: null,
}
}
const nonceNumber = tokenNonceState.result?.[0]?.toNumber()
if (tokenNonceState.loading || typeof nonceNumber !== 'number') {
return {
state: UseERC20PermitState.LOADING,
signatureData: null,
gatherPermitSignature: null,
}
}
const isSignatureDataValid =
signatureData &&
signatureData.owner === account &&
signatureData.deadline >= transactionDeadline.toNumber() &&
signatureData.tokenAddress === tokenAddress &&
signatureData.spender === spender &&
JSBI.equal(JSBI.BigInt(signatureData.amount), currencyAmount.raw)
return {
state: isSignatureDataValid ? UseERC20PermitState.SIGNED : UseERC20PermitState.NOT_SIGNED,
signatureData: isSignatureDataValid ? signatureData : null,
gatherPermitSignature: async function gatherPermitSignature() {
const signatureDeadline = transactionDeadline.toNumber() + PERMIT_VALIDITY_BUFFER
const value = currencyAmount.raw.toString()
const message = {
owner: account,
spender,
value,
nonce: nonceNumber,
deadline: signatureDeadline,
}
const domain = permitInfo.version
? {
name: permitInfo.name,
version: permitInfo.version,
verifyingContract: tokenAddress,
chainId,
}
: {
name: permitInfo.name,
verifyingContract: tokenAddress,
chainId,
}
const data = JSON.stringify({
types: {
EIP712Domain: permitInfo.version ? EIP712_DOMAIN_TYPE : EIP712_DOMAIN_TYPE_NO_VERSION,
Permit: EIP2612_TYPE,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: signatureDeadline,
amount: value,
nonce: nonceNumber,
chainId,
owner: account,
spender,
tokenAddress,
})
})
},
}
}, [
currencyAmount,
eip2612Contract,
account,
chainId,
isArgentWallet,
transactionDeadline,
library,
tokenNonceState.loading,
tokenNonceState.valid,
tokenNonceState.result,
tokenAddress,
spender,
permitInfo,
signatureData,
])
}
const REMOVE_V2_LIQUIDITY_PERMIT_INFO: PermitInfo = {
version: '1',
name: 'Uniswap V2',
type: PermitType.AMOUNT,
}
export function useV2LiquidityTokenPermit(
liquidityAmount: TokenAmount | null | undefined,
spender: string | null | undefined
) {
return useERC20Permit(liquidityAmount, spender, REMOVE_V2_LIQUIDITY_PERMIT_INFO)
}
export function useERC20PermitFromTrade(trade: V2Trade | V3Trade | undefined, allowedSlippage: number) {
const { chainId } = useActiveWeb3React()
const swapRouterAddress = SWAP_ROUTER_ADDRESSES[chainId as ChainId]
const amountToApprove = useMemo(
() => (trade ? trade.maximumAmountIn(new Percent(allowedSlippage, 10_000)) : undefined),
[trade, allowedSlippage]
)
return useERC20Permit(
amountToApprove,
// v2 router does not support
trade instanceof V2Trade ? undefined : trade instanceof V3Trade ? swapRouterAddress : undefined,
null
)
}
...@@ -12,6 +12,7 @@ import { calculateGasMargin, isAddress, shortenAddress } from '../utils' ...@@ -12,6 +12,7 @@ import { calculateGasMargin, isAddress, shortenAddress } from '../utils'
import isZero from '../utils/isZero' import isZero from '../utils/isZero'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import { useV2RouterContract } from './useContract' import { useV2RouterContract } from './useContract'
import { SignatureData } from './useERC20Permit'
import useTransactionDeadline from './useTransactionDeadline' import useTransactionDeadline from './useTransactionDeadline'
import useENS from './useENS' import useENS from './useENS'
import { Version } from './useToggledVersion' import { Version } from './useToggledVersion'
...@@ -46,12 +47,14 @@ interface FailedCall extends SwapCallEstimate { ...@@ -46,12 +47,14 @@ interface FailedCall extends SwapCallEstimate {
* Returns the swap calls that can be used to make the trade * Returns the swap calls that can be used to make the trade
* @param trade trade to execute * @param trade trade to execute
* @param allowedSlippage user allowed slippage * @param allowedSlippage user allowed slippage
* @param recipientAddressOrName * @param recipientAddressOrName the ENS name or address of the recipient of the swap output
* @param signatureData the signature data of the permit of the input token amount, if available
*/ */
function useSwapCallArguments( function useSwapCallArguments(
trade: V2Trade | V3Trade | undefined, // trade to execute, required trade: V2Trade | V3Trade | undefined, // trade to execute, required
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // 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 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[] { ): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
...@@ -99,6 +102,17 @@ function useSwapCallArguments( ...@@ -99,6 +102,17 @@ function useSwapCallArguments(
recipient, recipient,
slippageTolerance: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), slippageTolerance: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
deadline: deadline.toString(), deadline: deadline.toString(),
...(signatureData
? {
inputTokenPermit: {
deadline: signatureData.deadline,
amount: signatureData.amount,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
},
}
: {}),
}) })
return [ return [
...@@ -109,7 +123,7 @@ function useSwapCallArguments( ...@@ -109,7 +123,7 @@ function useSwapCallArguments(
}, },
] ]
} }
}, [account, allowedSlippage, chainId, deadline, library, recipient, routerContract, trade]) }, [account, allowedSlippage, chainId, deadline, library, recipient, routerContract, signatureData, trade])
} }
// returns a function that will execute a swap, if the parameters are all valid // returns a function that will execute a swap, if the parameters are all valid
...@@ -117,11 +131,12 @@ function useSwapCallArguments( ...@@ -117,11 +131,12 @@ function useSwapCallArguments(
export function useSwapCallback( export function useSwapCallback(
trade: V2Trade | V3Trade | undefined, // trade to execute, required trade: V2Trade | V3Trade | undefined, // trade to execute, required
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // 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 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 } { ): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: string | null } {
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName) const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName, signatureData)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
...@@ -231,9 +246,7 @@ export function useSwapCallback( ...@@ -231,9 +246,7 @@ export function useSwapCallback(
to: address, to: address,
data: calldata, data: calldata,
// let the wallet try if we can't estimate the gas // let the wallet try if we can't estimate the gas
...('gasEstimate' in bestCallOption ...('gasEstimate' in bestCallOption ? { gasLimit: calculateGasMargin(bestCallOption.gasEstimate) } : {}),
? { gasLimit: calculateGasMargin(bestCallOption.gasEstimate) }
: { gasLimit: 1_000_000 }),
...(value && !isZero(value) ? { value } : {}), ...(value && !isZero(value) ? { value } : {}),
}) })
.then((response) => { .then((response) => {
......
...@@ -8,6 +8,7 @@ import CurrencyLogo from '../../components/CurrencyLogo' ...@@ -8,6 +8,7 @@ import CurrencyLogo from '../../components/CurrencyLogo'
import FormattedCurrencyAmount from '../../components/FormattedCurrencyAmount' import FormattedCurrencyAmount from '../../components/FormattedCurrencyAmount'
import QuestionHelper from '../../components/QuestionHelper' import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row' import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { useV2LiquidityTokenPermit } from '../../hooks/useERC20Permit'
import { useTotalSupply } from '../../hooks/useTotalSupply' import { useTotalSupply } from '../../hooks/useTotalSupply'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens' import { useToken } from '../../hooks/Tokens'
...@@ -36,10 +37,7 @@ import { AlertTriangle, ChevronDown } from 'react-feather' ...@@ -36,10 +37,7 @@ import { AlertTriangle, ChevronDown } from 'react-feather'
import FeeSelector from 'components/FeeSelector' import FeeSelector from 'components/FeeSelector'
import RangeSelector from 'components/RangeSelector' import RangeSelector from 'components/RangeSelector'
import RateToggle from 'components/RateToggle' import RateToggle from 'components/RateToggle'
import useIsArgentWallet from 'hooks/useIsArgentWallet'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { splitSignature } from '@ethersproject/bytes'
import { BigNumber } from '@ethersproject/bignumber'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import { formatTokenAmount } from 'utils/formatTokenAmount' import { formatTokenAmount } from 'utils/formatTokenAmount'
import useTheme from 'hooks/useTheme' import useTheme from 'hooks/useTheme'
...@@ -104,7 +102,7 @@ function V2PairMigration({ ...@@ -104,7 +102,7 @@ function V2PairMigration({
token1: Token token1: Token
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { chainId, account, library } = useActiveWeb3React() const { chainId, account } = useActiveWeb3React()
const theme = useTheme() const theme = useTheme()
const deadline = useTransactionDeadline() // custom from users settings const deadline = useTransactionDeadline() // custom from users settings
...@@ -215,79 +213,13 @@ function V2PairMigration({ ...@@ -215,79 +213,13 @@ function V2PairMigration({
const migrator = useV2MigratorContract() const migrator = useV2MigratorContract()
// approvals // approvals
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: BigNumber } | null>(
null
)
const migratorAddress = chainId && V3_MIGRATOR_ADDRESSES[chainId] const migratorAddress = chainId && V3_MIGRATOR_ADDRESSES[chainId]
const [approval, approveManually] = useApproveCallback(pairBalance, migratorAddress) const [approval, approveManually] = useApproveCallback(pairBalance, migratorAddress)
const isArgentWallet = useIsArgentWallet() const { signatureData, gatherPermitSignature } = useV2LiquidityTokenPermit(pairBalance, migratorAddress)
const approve = useCallback(async () => { const approve = useCallback(async () => {
if (!account || !migrator || !deadline || !chainId || !library) return gatherPermitSignature ? gatherPermitSignature() : approveManually ? approveManually() : null
}, [gatherPermitSignature, approveManually])
if (isArgentWallet) {
return approveManually()
}
// try to gather a signature for permission
const nonce = await pair.nonces(account)
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Uniswap V2',
version: '1',
chainId: chainId,
verifyingContract: pair.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: migrator.address,
value: `0x${pairBalance.raw.toString(16)}`,
nonce: nonce.toHexString(),
deadline: deadline.toHexString(),
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline,
})
})
.catch((error) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
console.log('here?')
approveManually()
}
})
}, [account, isArgentWallet, approveManually, pair, pairBalance, migrator, deadline, chainId, library])
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const isMigrationPending = useIsTransactionPending(pendingMigrationHash ?? undefined) const isMigrationPending = useIsTransactionPending(pendingMigrationHash ?? undefined)
...@@ -307,11 +239,6 @@ function V2PairMigration({ ...@@ -307,11 +239,6 @@ function V2PairMigration({
const deadlineToUse = signatureData?.deadline ?? deadline const deadlineToUse = signatureData?.deadline ?? deadline
// janky way to ensure that stale-ish sigs are cleared
if (deadline.sub(deadlineToUse).lt(deadline.sub(blockTimestamp).div(2))) {
setSignatureData(null)
}
const data = [] const data = []
// permit if necessary // permit if necessary
...@@ -365,8 +292,6 @@ function V2PairMigration({ ...@@ -365,8 +292,6 @@ function V2PairMigration({
migrator migrator
.multicall(data) .multicall(data)
.then((response: TransactionResponse) => { .then((response: TransactionResponse) => {
setSignatureData(null) // clear sig data
ReactGA.event({ ReactGA.event({
category: 'Migrate', category: 'Migrate',
action: 'V2->V3', action: 'V2->V3',
......
import { splitSignature } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
import { Currency, currencyEquals, ETHER, Percent, WETH9 } from '@uniswap/sdk-core' import { Currency, currencyEquals, ETHER, Percent, WETH9 } from '@uniswap/sdk-core'
...@@ -24,7 +23,7 @@ import { V2_ROUTER_ADDRESS } from '../../constants' ...@@ -24,7 +23,7 @@ import { V2_ROUTER_ADDRESS } from '../../constants'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens' import { useCurrency } from '../../hooks/Tokens'
import { usePairContract, useV2RouterContract } from '../../hooks/useContract' import { usePairContract, useV2RouterContract } from '../../hooks/useContract'
import useIsArgentWallet from '../../hooks/useIsArgentWallet' import { useV2LiquidityTokenPermit } from '../../hooks/useERC20Permit'
import useTransactionDeadline from '../../hooks/useTransactionDeadline' import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { useTransactionAdder } from '../../state/transactions/hooks' import { useTransactionAdder } from '../../state/transactions/hooks'
...@@ -99,82 +98,23 @@ export default function RemoveLiquidity({ ...@@ -99,82 +98,23 @@ export default function RemoveLiquidity({
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address) const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling // allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null) const { gatherPermitSignature, signatureData } = useV2LiquidityTokenPermit(
parsedAmounts[Field.LIQUIDITY],
V2_ROUTER_ADDRESS
)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], V2_ROUTER_ADDRESS) const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], V2_ROUTER_ADDRESS)
const isArgentWallet = useIsArgentWallet()
async function onAttemptToApprove() { async function onAttemptToApprove() {
if (!pairContract || !pair || !library || !deadline) throw new Error('missing dependencies') if (!pairContract || !pair || !library || !deadline) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY] const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount') if (!liquidityAmount) throw new Error('missing liquidity amount')
if (isArgentWallet) { return gatherPermitSignature ? gatherPermitSignature() : approveCallback()
return approveCallback()
}
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Uniswap V2',
version: '1',
chainId: chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: V2_ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadline.toNumber(),
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadline.toNumber(),
})
})
.catch((error) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
} }
// wrapped onUserInput to clear signatures // wrapped onUserInput to clear signatures
const onUserInput = useCallback( const onUserInput = useCallback(
(field: Field, typedValue: string) => { (field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue) return _onUserInput(field, typedValue)
}, },
[_onUserInput] [_onUserInput]
...@@ -460,7 +400,6 @@ export default function RemoveLiquidity({ ...@@ -460,7 +400,6 @@ export default function RemoveLiquidity({
const handleDismissConfirmation = useCallback(() => { const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false) setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input // if there was a tx hash, we want to clear the input
if (txHash) { if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0') onUserInput(Field.LIQUIDITY_PERCENT, '0')
......
...@@ -30,6 +30,7 @@ import { useAllTokens, useCurrency } from '../../hooks/Tokens' ...@@ -30,6 +30,7 @@ import { useAllTokens, useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback' import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import { V3TradeState } from '../../hooks/useBestV3Trade' import { V3TradeState } from '../../hooks/useBestV3Trade'
import useENSAddress from '../../hooks/useENSAddress' import useENSAddress from '../../hooks/useENSAddress'
import { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported' import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useSwapCallback } from '../../hooks/useSwapCallback' import { useSwapCallback } from '../../hooks/useSwapCallback'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion' import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
...@@ -186,6 +187,15 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -186,6 +187,15 @@ export default function Swap({ history }: RouteComponentProps) {
// check whether the user has approved the router on the input token // check whether the user has approved the router on the input token
const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage) const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage)
const { state: signatureState, signatureData, gatherPermitSignature } = useERC20PermitFromTrade(
trade,
allowedSlippage
)
const handleApprove = useCallback(() => {
if (signatureState === UseERC20PermitState.NOT_SIGNED && gatherPermitSignature) gatherPermitSignature()
else approveCallback()
}, [approveCallback, gatherPermitSignature, signatureState])
// check if user has gone through approval process, used to show two step buttons, reset on token change // check if user has gone through approval process, used to show two step buttons, reset on token change
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false) const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
...@@ -201,7 +211,12 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -201,7 +211,12 @@ export default function Swap({ history }: RouteComponentProps) {
const atMaxInputAmount = Boolean(maxInputAmount && parsedAmounts[Field.INPUT]?.equalTo(maxInputAmount)) const atMaxInputAmount = Boolean(maxInputAmount && parsedAmounts[Field.INPUT]?.equalTo(maxInputAmount))
// the callback to execute the swap // the callback to execute the swap
const { callback: swapCallback, error: swapCallbackError } = useSwapCallback(trade, allowedSlippage, recipient) const { callback: swapCallback, error: swapCallbackError } = useSwapCallback(
trade,
allowedSlippage,
recipient,
signatureData
)
const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade) const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade)
...@@ -426,11 +441,17 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -426,11 +441,17 @@ export default function Swap({ history }: RouteComponentProps) {
<AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}> <AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}>
<AutoColumn style={{ width: '100%' }} gap="12px"> <AutoColumn style={{ width: '100%' }} gap="12px">
<ButtonConfirmed <ButtonConfirmed
onClick={approveCallback} onClick={handleApprove}
disabled={approvalState !== ApprovalState.NOT_APPROVED || approvalSubmitted} disabled={
approvalState !== ApprovalState.NOT_APPROVED ||
approvalSubmitted ||
signatureState === UseERC20PermitState.SIGNED
}
width="100%" width="100%"
altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
confirmed={approvalState === ApprovalState.APPROVED} confirmed={
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED
}
> >
<AutoRow justify="space-between"> <AutoRow justify="space-between">
<span style={{ display: 'flex', alignItems: 'center' }}> <span style={{ display: 'flex', alignItems: 'center' }}>
...@@ -439,12 +460,13 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -439,12 +460,13 @@ export default function Swap({ history }: RouteComponentProps) {
size={'16px'} size={'16px'}
style={{ marginRight: '8px' }} style={{ marginRight: '8px' }}
/> />
{/* we need to shorted this string on mobile */} {/* we need to shorten this string on mobile */}
{'Allow Uniswap to spend your ' + currencies[Field.INPUT]?.symbol} {'Allow Uniswap to spend your ' + currencies[Field.INPUT]?.symbol}
</span> </span>
{approvalState === ApprovalState.PENDING ? ( {approvalState === ApprovalState.PENDING ? (
<Loader stroke="white" /> <Loader stroke="white" />
) : approvalSubmitted && approvalState === ApprovalState.APPROVED ? ( ) : (approvalSubmitted && approvalState === ApprovalState.APPROVED) ||
signatureState === UseERC20PermitState.SIGNED ? (
<Unlock size="16" stroke="white" /> <Unlock size="16" stroke="white" />
) : ( ) : (
<Unlock size="16" stroke="white" /> <Unlock size="16" stroke="white" />
...@@ -467,7 +489,11 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -467,7 +489,11 @@ export default function Swap({ history }: RouteComponentProps) {
}} }}
width="100%" width="100%"
id="swap-button" id="swap-button"
disabled={!isValid || approvalState !== ApprovalState.APPROVED || priceImpactTooHigh} disabled={
!isValid ||
(approvalState !== ApprovalState.APPROVED && signatureState !== UseERC20PermitState.SIGNED) ||
priceImpactTooHigh
}
error={isValid && priceImpactSeverity > 2} error={isValid && priceImpactSeverity > 2}
> >
<Text fontSize={16} fontWeight={500}> <Text fontSize={16} fontWeight={500}>
...@@ -475,7 +501,13 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -475,7 +501,13 @@ export default function Swap({ history }: RouteComponentProps) {
</Text> </Text>
</ButtonError> </ButtonError>
</AutoColumn> </AutoColumn>
{showApproveFlow && <ProgressSteps steps={[approvalState === ApprovalState.APPROVED]} />} {showApproveFlow && (
<ProgressSteps
steps={[
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED,
]}
/>
)}
</AutoRow> </AutoRow>
) : ( ) : (
<ButtonError <ButtonError
......
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