feat: use multicall for argent wallets in swap and v3 add liquidity (#1387)

* use argent wallet contract in swap callback

* maybe working swap callback

* chore(v3): trigger a breaking release

BREAKING CHANGE: trigger a major release for the uniswap interface to indicate it now supports swapping and liquidity provision against uniswap protocol v3

* fix the value

* improve the error coverage

* retry more frequently, couple more error nits

* the is argent call was being sketchy

* get it working for add liquidity

* `0x0` for v2 swaps too

* small nits in position page

* fix import

* fix compiler error
This commit is contained in:
Moody Salem 2021-05-21 09:30:07 -05:00 committed by GitHub
parent 99ad4ae44c
commit 562b402293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 217 additions and 20 deletions

@ -0,0 +1,61 @@
[
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "_transactions",
"type": "tuple[]"
}
],
"name": "wc_multiCall",
"outputs": [
{
"internalType": "bytes[]",
"name": "",
"type": "bytes[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "_msgHash",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "_signature",
"type": "bytes"
}
],
"name": "isValidSignature",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "view",
"type": "function"
}
]

@ -0,0 +1,15 @@
import { ArgentWalletContract } from '../abis/types'
import { useActiveWeb3React } from './web3'
import { useContract } from './useContract'
import useIsArgentWallet from './useIsArgentWallet'
import ArgentWalletContractABI from '../abis/argent-wallet-contract.json'
export function useArgentWalletContract(): ArgentWalletContract | null {
const { account } = useActiveWeb3React()
const isArgentWallet = useIsArgentWallet()
return useContract(
isArgentWallet ? account ?? undefined : undefined,
ArgentWalletContractABI,
true
) as ArgentWalletContract
}

@ -41,7 +41,7 @@ import { Quoter, UniswapV3Factory, UniswapV3Pool } from 'types/v3'
import { NonfungiblePositionManager } from 'types/v3/NonfungiblePositionManager' import { NonfungiblePositionManager } from 'types/v3/NonfungiblePositionManager'
import { V3Migrator } from 'types/v3/V3Migrator' import { V3Migrator } from 'types/v3/V3Migrator'
import { getContract } from 'utils' import { getContract } from 'utils'
import { ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Erc20, Multicall2, Weth } from '../abis/types' import { Erc20, ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Multicall2, Weth } from '../abis/types'
import { UNI } from '../constants/tokens' import { UNI } from '../constants/tokens'
import { useActiveWeb3React } from './web3' import { useActiveWeb3React } from './web3'

@ -1,3 +1,4 @@
import { useMemo } from 'react'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks' import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useActiveWeb3React } from './web3' import { useActiveWeb3React } from './web3'
import { useArgentWalletDetectorContract } from './useContract' import { useArgentWalletDetectorContract } from './useContract'
@ -5,6 +6,7 @@ import { useArgentWalletDetectorContract } from './useContract'
export default function useIsArgentWallet(): boolean { export default function useIsArgentWallet(): boolean {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const argentWalletDetector = useArgentWalletDetectorContract() const argentWalletDetector = useArgentWalletDetectorContract()
const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', [account ?? undefined], NEVER_RELOAD) const inputs = useMemo(() => [account ?? undefined], [account])
const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', inputs, NEVER_RELOAD)
return call?.result?.[0] ?? false return call?.result?.[0] ?? false
} }

@ -8,7 +8,8 @@ export default function useSocksBalance(): JSBI | undefined {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const socksContract = useSocksController() const socksContract = useSocksController()
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD) const inputs = useMemo(() => [account ?? undefined], [account])
const { result } = useSingleCallResult(socksContract, 'balanceOf', inputs, NEVER_RELOAD)
const data = result?.[0] const data = result?.[0]
return data ? JSBI.BigInt(data.toString()) : undefined return data ? JSBI.BigInt(data.toString()) : undefined
} }

@ -5,11 +5,13 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react' import { useMemo } from 'react'
import { SWAP_ROUTER_ADDRESSES } from '../constants/addresses' import { SWAP_ROUTER_ADDRESSES } from '../constants/addresses'
import { calculateGasMargin } from '../utils/calculateGasMargin' import { calculateGasMargin } from '../utils/calculateGasMargin'
import approveAmountCalldata from '../utils/approveAmountCalldata'
import { getTradeVersion } from '../utils/getTradeVersion' import { getTradeVersion } from '../utils/getTradeVersion'
import { useTransactionAdder } from '../state/transactions/hooks' import { useTransactionAdder } from '../state/transactions/hooks'
import { isAddress, shortenAddress } from '../utils' import { isAddress, shortenAddress } from '../utils'
import isZero from '../utils/isZero' import isZero from '../utils/isZero'
import { useActiveWeb3React } from './web3' import { useActiveWeb3React } from './web3'
import { useArgentWalletContract } from './useArgentWalletContract'
import { useV2RouterContract } from './useContract' import { useV2RouterContract } from './useContract'
import { SignatureData } from './useERC20Permit' import { SignatureData } from './useERC20Permit'
import useTransactionDeadline from './useTransactionDeadline' import useTransactionDeadline from './useTransactionDeadline'
@ -61,6 +63,7 @@ function useSwapCallArguments(
const recipient = recipientAddressOrName === null ? account : recipientAddress const recipient = recipientAddressOrName === null ? account : recipientAddress
const deadline = useTransactionDeadline() const deadline = useTransactionDeadline()
const routerContract = useV2RouterContract() const routerContract = useV2RouterContract()
const argentWalletContract = useArgentWalletContract()
return useMemo(() => { return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline) return [] if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
@ -68,6 +71,7 @@ function useSwapCallArguments(
if (trade instanceof V2Trade) { if (trade instanceof V2Trade) {
if (!routerContract) return [] if (!routerContract) return []
const swapMethods = [] const swapMethods = []
swapMethods.push( swapMethods.push(
Router.swapCallParameters(trade, { Router.swapCallParameters(trade, {
feeOnTransfer: false, feeOnTransfer: false,
@ -87,11 +91,30 @@ function useSwapCallArguments(
}) })
) )
} }
return swapMethods.map(({ methodName, args, value }) => ({ return swapMethods.map(({ methodName, args, value }) => {
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return {
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
{
to: routerContract.address,
value: value,
data: routerContract.interface.encodeFunctionData(methodName, args),
},
],
]),
value: '0x0',
}
} else {
return {
address: routerContract.address, address: routerContract.address,
calldata: routerContract.interface.encodeFunctionData(methodName, args), calldata: routerContract.interface.encodeFunctionData(methodName, args),
value, value,
})) }
}
})
} else { } else {
// trade is V3Trade // trade is V3Trade
const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
@ -122,7 +145,24 @@ function useSwapCallArguments(
} }
: {}), : {}),
}) })
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return [
{
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
{
to: swapRouterAddress,
value: value,
data: calldata,
},
],
]),
value: '0x0',
},
]
}
return [ return [
{ {
address: swapRouterAddress, address: swapRouterAddress,
@ -131,7 +171,18 @@ function useSwapCallArguments(
}, },
] ]
} }
}, [account, allowedSlippage, chainId, deadline, library, recipient, routerContract, signatureData, trade]) }, [
account,
allowedSlippage,
argentWalletContract,
chainId,
deadline,
library,
recipient,
routerContract,
signatureData,
trade,
])
} }
/** /**

@ -6,6 +6,7 @@ import { AlertTriangle, AlertCircle } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { ZERO_PERCENT } from '../../constants/misc' import { ZERO_PERCENT } from '../../constants/misc'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from '../../constants/addresses' import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from '../../constants/addresses'
import { useArgentWalletContract } from '../../hooks/useArgentWalletContract'
import { useV3NFTPositionManagerContract } from '../../hooks/useContract' import { useV3NFTPositionManagerContract } from '../../hooks/useContract'
import { RouteComponentProps } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
@ -18,6 +19,7 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { RowBetween, RowFixed } from '../../components/Row' import { RowBetween, RowFixed } from '../../components/Row'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported' import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useUSDCValue } from '../../hooks/useUSDCPrice' import { useUSDCValue } from '../../hooks/useUSDCPrice'
import approveAmountCalldata from '../../utils/approveAmountCalldata'
import { calculateGasMargin } from '../../utils/calculateGasMargin' import { calculateGasMargin } from '../../utils/calculateGasMargin'
import Review from './Review' import Review from './Review'
import { useActiveWeb3React } from '../../hooks/web3' import { useActiveWeb3React } from '../../hooks/web3'
@ -170,13 +172,15 @@ export default function AddLiquidity({
{} {}
) )
const argentWalletContract = useArgentWalletContract()
// check whether the user has approved the router on the tokens // check whether the user has approved the router on the tokens
const [approvalA, approveACallback] = useApproveCallback( const [approvalA, approveACallback] = useApproveCallback(
parsedAmounts[Field.CURRENCY_A], argentWalletContract ? undefined : parsedAmounts[Field.CURRENCY_A],
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
) )
const [approvalB, approveBCallback] = useApproveCallback( const [approvalB, approveBCallback] = useApproveCallback(
parsedAmounts[Field.CURRENCY_B], argentWalletContract ? undefined : parsedAmounts[Field.CURRENCY_B],
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
) )
@ -208,12 +212,36 @@ export default function AddLiquidity({
createPool: noLiquidity, createPool: noLiquidity,
}) })
const txn = { let txn: { to: string; data: string; value: string } = {
to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId], to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId],
data: calldata, data: calldata,
value, value,
} }
if (argentWalletContract) {
const amountA = parsedAmounts[Field.CURRENCY_A]
const amountB = parsedAmounts[Field.CURRENCY_B]
const batch = [
...(amountA && amountA.currency.isToken
? [approveAmountCalldata(amountA, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId])]
: []),
...(amountB && amountB.currency.isToken
? [approveAmountCalldata(amountB, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId])]
: []),
{
to: txn.to,
data: txn.data,
value: txn.value,
},
]
const data = argentWalletContract.interface.encodeFunctionData('wc_multiCall', [batch])
txn = {
to: argentWalletContract.address,
data,
value: '0x0',
}
}
setAttemptingTxn(true) setAttemptingTxn(true)
library library
@ -244,6 +272,7 @@ export default function AddLiquidity({
}) })
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to send transaction', error)
setAttemptingTxn(false) setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx // we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) { if (error?.code !== 4001) {
@ -351,8 +380,10 @@ export default function AddLiquidity({
) )
// we need an existence check on parsed amounts for single-asset deposits // we need an existence check on parsed amounts for single-asset deposits
const showApprovalA = approvalA !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_A] const showApprovalA =
const showApprovalB = approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B] !argentWalletContract && approvalA !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_A]
const showApprovalB =
!argentWalletContract && approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B]
return ( return (
<ScrollablePage> <ScrollablePage>
@ -418,7 +449,7 @@ export default function AddLiquidity({
id="add-liquidity-input-tokena" id="add-liquidity-input-tokena"
showCommonBases showCommonBases
/> />
<div style={{ width: '12px' }}></div> <div style={{ width: '12px' }} />
<CurrencyDropdown <CurrencyDropdown
value={formattedAmounts[Field.CURRENCY_B]} value={formattedAmounts[Field.CURRENCY_B]}
@ -571,7 +602,7 @@ export default function AddLiquidity({
<TYPE.body fontWeight={500} textAlign="center" fontSize={20}> <TYPE.body fontWeight={500} textAlign="center" fontSize={20}>
<HoverInlineText <HoverInlineText
maxCharacters={20} maxCharacters={20}
text={invertPrice ? price.invert().toSignificant(5) : price.toSignificant(5)} text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)}
/>{' '} />{' '}
</TYPE.body> </TYPE.body>
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}> <TYPE.main fontWeight={500} textAlign="center" fontSize={12}>
@ -697,8 +728,8 @@ export default function AddLiquidity({
}} }}
disabled={ disabled={
!isValid || !isValid ||
(approvalA !== ApprovalState.APPROVED && !depositADisabled) || (!argentWalletContract && approvalA !== ApprovalState.APPROVED && !depositADisabled) ||
(approvalB !== ApprovalState.APPROVED && !depositBDisabled) (!argentWalletContract && approvalB !== ApprovalState.APPROVED && !depositBDisabled)
} }
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]} error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
> >

@ -166,7 +166,7 @@ function CurrentPriceCard({
<AutoColumn gap="8px" justify="center"> <AutoColumn gap="8px" justify="center">
<ExtentsText>{t('Current price')}</ExtentsText> <ExtentsText>{t('Current price')}</ExtentsText>
<TYPE.mediumHeader textAlign="center"> <TYPE.mediumHeader textAlign="center">
{(inverted ? pool.token1Price : pool.token0Price).toSignificant(5)}{' '} {(inverted ? pool.token1Price : pool.token0Price).toSignificant(6)}{' '}
</TYPE.mediumHeader> </TYPE.mediumHeader>
<ExtentsText>{currencyQuote?.symbol + ' per ' + currencyBase?.symbol}</ExtentsText> <ExtentsText>{currencyQuote?.symbol + ' per ' + currencyBase?.symbol}</ExtentsText>
</AutoColumn> </AutoColumn>

@ -33,6 +33,7 @@ import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useAppro
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 { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit'
import useIsArgentWallet from '../../hooks/useIsArgentWallet'
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'
@ -302,9 +303,12 @@ export default function Swap({ history }: RouteComponentProps) {
) )
}, [priceImpact, trade]) }, [priceImpact, trade])
const isArgentWallet = useIsArgentWallet()
// show approve flow when: no error on inputs, not approved or pending, or approved in current session // show approve flow when: no error on inputs, not approved or pending, or approved in current session
// never show if price impact is above threshold in non expert mode // never show if price impact is above threshold in non expert mode
const showApproveFlow = const showApproveFlow =
!isArgentWallet &&
!swapInputError && !swapInputError &&
(approvalState === ApprovalState.NOT_APPROVED || (approvalState === ApprovalState.NOT_APPROVED ||
approvalState === ApprovalState.PENDING || approvalState === ApprovalState.PENDING ||

@ -0,0 +1,32 @@
import { Interface } from '@ethersproject/abi'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { toHex } from '@uniswap/v3-sdk'
import { Erc20Interface } from '../abis/types/Erc20'
const ERC20_INTERFACE = new Interface([
{
constant: false,
inputs: [
{ name: '_spender', type: 'address' },
{ name: '_value', type: 'uint256' },
],
name: 'approve',
outputs: [{ name: '', type: 'bool' }],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
]) as Erc20Interface
export default function approveAmountCalldata(
amount: CurrencyAmount<Currency>,
spender: string
): { to: string; data: string; value: '0x0' } {
if (!amount.currency.isToken) throw new Error('Must call with an amount of token')
const approveData = ERC20_INTERFACE.encodeFunctionData('approve', [spender, toHex(amount.quotient)])
return {
to: amount.currency.address,
data: approveData,
value: '0x0',
}
}