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 { V3Migrator } from 'types/v3/V3Migrator'
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 { useActiveWeb3React } from './web3'

@ -1,3 +1,4 @@
import { useMemo } from 'react'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useActiveWeb3React } from './web3'
import { useArgentWalletDetectorContract } from './useContract'
@ -5,6 +6,7 @@ import { useArgentWalletDetectorContract } from './useContract'
export default function useIsArgentWallet(): boolean {
const { account } = useActiveWeb3React()
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
}

@ -8,7 +8,8 @@ export default function useSocksBalance(): JSBI | undefined {
const { account } = useActiveWeb3React()
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]
return data ? JSBI.BigInt(data.toString()) : undefined
}

@ -5,11 +5,13 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { SWAP_ROUTER_ADDRESSES } from '../constants/addresses'
import { calculateGasMargin } from '../utils/calculateGasMargin'
import approveAmountCalldata from '../utils/approveAmountCalldata'
import { getTradeVersion } from '../utils/getTradeVersion'
import { useTransactionAdder } from '../state/transactions/hooks'
import { isAddress, shortenAddress } from '../utils'
import isZero from '../utils/isZero'
import { useActiveWeb3React } from './web3'
import { useArgentWalletContract } from './useArgentWalletContract'
import { useV2RouterContract } from './useContract'
import { SignatureData } from './useERC20Permit'
import useTransactionDeadline from './useTransactionDeadline'
@ -61,6 +63,7 @@ function useSwapCallArguments(
const recipient = recipientAddressOrName === null ? account : recipientAddress
const deadline = useTransactionDeadline()
const routerContract = useV2RouterContract()
const argentWalletContract = useArgentWalletContract()
return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
@ -68,6 +71,7 @@ function useSwapCallArguments(
if (trade instanceof V2Trade) {
if (!routerContract) return []
const swapMethods = []
swapMethods.push(
Router.swapCallParameters(trade, {
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,
calldata: routerContract.interface.encodeFunctionData(methodName, args),
value,
}))
}
}
})
} else {
// trade is V3Trade
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 [
{
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 { ZERO_PERCENT } from '../../constants/misc'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from '../../constants/addresses'
import { useArgentWalletContract } from '../../hooks/useArgentWalletContract'
import { useV3NFTPositionManagerContract } from '../../hooks/useContract'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
@ -18,6 +19,7 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { RowBetween, RowFixed } from '../../components/Row'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useUSDCValue } from '../../hooks/useUSDCPrice'
import approveAmountCalldata from '../../utils/approveAmountCalldata'
import { calculateGasMargin } from '../../utils/calculateGasMargin'
import Review from './Review'
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
const [approvalA, approveACallback] = useApproveCallback(
parsedAmounts[Field.CURRENCY_A],
argentWalletContract ? undefined : parsedAmounts[Field.CURRENCY_A],
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
)
const [approvalB, approveBCallback] = useApproveCallback(
parsedAmounts[Field.CURRENCY_B],
argentWalletContract ? undefined : parsedAmounts[Field.CURRENCY_B],
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
)
@ -208,12 +212,36 @@ export default function AddLiquidity({
createPool: noLiquidity,
})
const txn = {
let txn: { to: string; data: string; value: string } = {
to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId],
data: calldata,
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)
library
@ -244,6 +272,7 @@ export default function AddLiquidity({
})
})
.catch((error) => {
console.error('Failed to send transaction', error)
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
@ -351,8 +380,10 @@ export default function AddLiquidity({
)
// we need an existence check on parsed amounts for single-asset deposits
const showApprovalA = approvalA !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_A]
const showApprovalB = approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B]
const showApprovalA =
!argentWalletContract && approvalA !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_A]
const showApprovalB =
!argentWalletContract && approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B]
return (
<ScrollablePage>
@ -418,7 +449,7 @@ export default function AddLiquidity({
id="add-liquidity-input-tokena"
showCommonBases
/>
<div style={{ width: '12px' }}></div>
<div style={{ width: '12px' }} />
<CurrencyDropdown
value={formattedAmounts[Field.CURRENCY_B]}
@ -571,7 +602,7 @@ export default function AddLiquidity({
<TYPE.body fontWeight={500} textAlign="center" fontSize={20}>
<HoverInlineText
maxCharacters={20}
text={invertPrice ? price.invert().toSignificant(5) : price.toSignificant(5)}
text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)}
/>{' '}
</TYPE.body>
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}>
@ -697,8 +728,8 @@ export default function AddLiquidity({
}}
disabled={
!isValid ||
(approvalA !== ApprovalState.APPROVED && !depositADisabled) ||
(approvalB !== ApprovalState.APPROVED && !depositBDisabled)
(!argentWalletContract && approvalA !== ApprovalState.APPROVED && !depositADisabled) ||
(!argentWalletContract && approvalB !== ApprovalState.APPROVED && !depositBDisabled)
}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>

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

@ -33,6 +33,7 @@ import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useAppro
import { V3TradeState } from '../../hooks/useBestV3Trade'
import useENSAddress from '../../hooks/useENSAddress'
import { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit'
import useIsArgentWallet from '../../hooks/useIsArgentWallet'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
@ -302,9 +303,12 @@ export default function Swap({ history }: RouteComponentProps) {
)
}, [priceImpact, trade])
const isArgentWallet = useIsArgentWallet()
// 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
const showApproveFlow =
!isArgentWallet &&
!swapInputError &&
(approvalState === ApprovalState.NOT_APPROVED ||
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',
}
}