From 05e476330accaf7aea6553e9ee391e47500e0efd Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:21:21 -0700 Subject: [PATCH 01/10] permit2 --- src/swap/components/SwapProvider.tsx | 1 + src/swap/constants.ts | 4 +++ src/swap/utils/processSwapTransaction.ts | 42 ++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/swap/components/SwapProvider.tsx b/src/swap/components/SwapProvider.tsx index b58e2d077e..66ee6d4d5b 100644 --- a/src/swap/components/SwapProvider.tsx +++ b/src/swap/components/SwapProvider.tsx @@ -172,6 +172,7 @@ export function SwapProvider({ sendTransactionAsync, onStart, onSuccess, + useAggregator, }); // TODO: refresh balances diff --git a/src/swap/constants.ts b/src/swap/constants.ts index 01cfe9500d..b7c6913ee3 100644 --- a/src/swap/constants.ts +++ b/src/swap/constants.ts @@ -6,3 +6,7 @@ export const TOO_MANY_REQUESTS_ERROR_CODE = 'TOO_MANY_REQUESTS_ERROR'; export const UNCAUGHT_SWAP_QUOTE_ERROR_CODE = 'UNCAUGHT_SWAP_QUOTE_ERROR'; export const UNCAUGHT_SWAP_ERROR_CODE = 'UNCAUGHT_SWAP_ERROR'; export const USER_REJECTED_ERROR_CODE = 'USER_REJECTED'; +export const PERMIT2_CONTRACT_ADDRESS = + '0x000000000022D473030F116dDEE9F6B43aC78BA3'; +export const UNIVERSALROUTER_CONTRACT_ADDRESS = + '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD'; diff --git a/src/swap/utils/processSwapTransaction.ts b/src/swap/utils/processSwapTransaction.ts index a069a038a0..7ab396c8d1 100644 --- a/src/swap/utils/processSwapTransaction.ts +++ b/src/swap/utils/processSwapTransaction.ts @@ -1,8 +1,13 @@ -import type { TransactionReceipt } from 'viem'; +import type { Address, TransactionReceipt } from 'viem'; import type { Config } from 'wagmi'; import { waitForTransactionReceipt } from 'wagmi/actions'; import type { SendTransactionMutateAsync } from 'wagmi/query'; +import { + UNIVERSALROUTER_CONTRACT_ADDRESS, + PERMIT2_CONTRACT_ADDRESS, +} from '../constants'; import type { BuildSwapTransaction } from '../types'; +import { encodeFunctionData, parseAbi } from 'viem'; export async function processSwapTransaction({ swapTransaction, @@ -12,6 +17,7 @@ export async function processSwapTransaction({ sendTransactionAsync, onStart, onSuccess, + useAggregator, }: { swapTransaction: BuildSwapTransaction; config: Config; @@ -22,8 +28,9 @@ export async function processSwapTransaction({ onSuccess: | ((txReceipt: TransactionReceipt) => void | Promise) | undefined; + useAggregator: boolean; }) { - const { transaction, approveTransaction } = swapTransaction; + const { transaction, approveTransaction, quote } = swapTransaction; // for swaps from ERC-20 tokens, // if there is an approveTransaction present, @@ -41,6 +48,37 @@ export async function processSwapTransaction({ confirmations: 1, }); setPendingTransaction(false); + + // for the V2 API, we use Uniswap's UniversalRouter + // this adds an additional transaction/step to the swap process + // the `approveTx` on the response will be an approval for the amount of the `from` token against `Permit2` + // we also need to make an extra transaction to `Permit2` to approve the UniversalRouter to spend the funds + // see more: https://blog.uniswap.org/permit2-and-universal-router + if (!useAggregator) { + const permit2ContractAbi = parseAbi([ + 'function approve(address token, address spender, uint160 amount, uint48 expiration) external', + ]); + const data = encodeFunctionData({ + abi: permit2ContractAbi, + functionName: 'approve', + args: [ + quote.from.address as Address, + UNIVERSALROUTER_CONTRACT_ADDRESS, + BigInt(quote.fromAmount), + 20_000_000_000_000, + ], + }); + const permitTxnHash = await sendTransactionAsync({ + to: PERMIT2_CONTRACT_ADDRESS, + data: data, + value: 0n, + }); + await Promise.resolve(onStart?.(permitTxnHash)); + await waitForTransactionReceipt(config, { + hash: permitTxnHash, + confirmations: 1, + }); + } } // make the swap From 92c304e2ba99b88bcc2f2381231191964b9c2e26 Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:04:04 -0700 Subject: [PATCH 02/10] better comments --- src/swap/utils/processSwapTransaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/swap/utils/processSwapTransaction.ts b/src/swap/utils/processSwapTransaction.ts index 7ab396c8d1..b357b17fc9 100644 --- a/src/swap/utils/processSwapTransaction.ts +++ b/src/swap/utils/processSwapTransaction.ts @@ -51,9 +51,9 @@ export async function processSwapTransaction({ // for the V2 API, we use Uniswap's UniversalRouter // this adds an additional transaction/step to the swap process - // the `approveTx` on the response will be an approval for the amount of the `from` token against `Permit2` + // the `approveTx` on the response will be an approval for the amount of the `from` token against `Permit2`, instead of an approval against the Router itself // we also need to make an extra transaction to `Permit2` to approve the UniversalRouter to spend the funds - // see more: https://blog.uniswap.org/permit2-and-universal-router + // read more: https://blog.uniswap.org/permit2-and-universal-router if (!useAggregator) { const permit2ContractAbi = parseAbi([ 'function approve(address token, address spender, uint160 amount, uint48 expiration) external', From 7b6fa13e930ab85fe7fde76e01014a7045ac2a39 Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:43:39 -0700 Subject: [PATCH 03/10] test coverage --- src/swap/utils/processSwapTransaction.test.ts | 105 ++++++++++++++++++ src/swap/utils/processSwapTransaction.ts | 2 + 2 files changed, 107 insertions(+) diff --git a/src/swap/utils/processSwapTransaction.test.ts b/src/swap/utils/processSwapTransaction.test.ts index c3e2fa9bb1..71745a8632 100644 --- a/src/swap/utils/processSwapTransaction.test.ts +++ b/src/swap/utils/processSwapTransaction.test.ts @@ -21,6 +21,11 @@ describe('processSwapTransaction', () => { .fn() .mockResolvedValueOnce('approveTxHash') .mockResolvedValueOnce('txHash'); + const sendTransactionAsyncPermit2 = vi + .fn() + .mockResolvedValueOnce('approveTxHash') + .mockResolvedValueOnce('permit2TxHash') + .mockResolvedValueOnce('txHash'); const onSuccess = vi.fn(); const onStart = vi.fn(); const onSuccessAsync = vi.fn().mockImplementation(async (_txHash: string) => { @@ -117,6 +122,7 @@ describe('processSwapTransaction', () => { sendTransactionAsync, onSuccess, onStart, + useAggregator: true, }); expect(setPendingTransaction).toHaveBeenCalledTimes(4); @@ -210,6 +216,7 @@ describe('processSwapTransaction', () => { sendTransactionAsync, onSuccess, onStart, + useAggregator: true, }); expect(setPendingTransaction).toHaveBeenCalledTimes(2); @@ -304,6 +311,7 @@ describe('processSwapTransaction', () => { sendTransactionAsync: sendTransactionAsync2, onSuccess: onSuccessAsync, onStart: onStartAsync, + useAggregator: true, }); expect(setPendingTransaction).toHaveBeenCalledTimes(4); @@ -320,4 +328,101 @@ describe('processSwapTransaction', () => { expect(onStartAsync).toHaveBeenNthCalledWith(1, 'approveTxHash'); expect(onStartAsync).toHaveBeenNthCalledWith(2, 'txHash'); }); + + it('should successfully use Permit2 approval process for `useAggregators`=false', async () => { + const swapTransaction: BuildSwapTransaction = { + transaction: { + to: '0x123', + value: 0n, + data: '0x', + chainId: 8453, + gas: 0n, + }, + approveTransaction: { + to: '0x456', + value: 0n, + data: '0x123', + chainId: 8453, + gas: 0n, + }, + quote: { + from: { + name: 'USDC', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + to: { + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + chainId: 8453, + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + name: 'DEGEN', + symbol: 'DEGEN', + }, + fromAmount: '100000000000000', + toAmount: '19395353519910973703', + amountReference: 'from', + priceImpact: '0.94', + hasHighPriceImpact: false, + slippage: '3', + warning: undefined, + }, + fee: { + baseAsset: { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }, + percentage: '1', + amount: '195912661817282562', + }, + }; + const config = createConfig({ + chains: [mainnet, sepolia], + connectors: [ + mock({ + accounts: [ + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', + ], + }), + ], + transports: { + [mainnet.id]: http(), + [sepolia.id]: http(), + }, + }); + + await processSwapTransaction({ + swapTransaction, + config, + setPendingTransaction, + setLoading, + sendTransactionAsync: sendTransactionAsyncPermit2, + onSuccess, + onStart, + useAggregator: false, + }); + + expect(setPendingTransaction).toHaveBeenCalledTimes(6); + expect(setPendingTransaction).toHaveBeenCalledWith(true); + expect(setPendingTransaction).toHaveBeenCalledWith(false); + expect(sendTransactionAsyncPermit2).toHaveBeenCalledTimes(3); + expect(waitForTransactionReceipt).toHaveBeenCalledTimes(3); + + expect(setLoading).toHaveBeenCalledTimes(1); + expect(setLoading).toHaveBeenCalledWith(true); + expect(onSuccess).toHaveBeenCalledTimes(1); + expect(onSuccess).toHaveBeenCalledWith({}); + }); }); diff --git a/src/swap/utils/processSwapTransaction.ts b/src/swap/utils/processSwapTransaction.ts index b357b17fc9..2767d11ddc 100644 --- a/src/swap/utils/processSwapTransaction.ts +++ b/src/swap/utils/processSwapTransaction.ts @@ -55,6 +55,7 @@ export async function processSwapTransaction({ // we also need to make an extra transaction to `Permit2` to approve the UniversalRouter to spend the funds // read more: https://blog.uniswap.org/permit2-and-universal-router if (!useAggregator) { + setPendingTransaction(true); const permit2ContractAbi = parseAbi([ 'function approve(address token, address spender, uint160 amount, uint48 expiration) external', ]); @@ -78,6 +79,7 @@ export async function processSwapTransaction({ hash: permitTxnHash, confirmations: 1, }); + setPendingTransaction(false); } } From c79a450b005923043023a0a50219b0d22cb8638d Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:47:26 -0700 Subject: [PATCH 04/10] add `onStart` arguments to test --- src/swap/utils/processSwapTransaction.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/swap/utils/processSwapTransaction.test.ts b/src/swap/utils/processSwapTransaction.test.ts index 71745a8632..318fee3a68 100644 --- a/src/swap/utils/processSwapTransaction.test.ts +++ b/src/swap/utils/processSwapTransaction.test.ts @@ -424,5 +424,9 @@ describe('processSwapTransaction', () => { expect(setLoading).toHaveBeenCalledWith(true); expect(onSuccess).toHaveBeenCalledTimes(1); expect(onSuccess).toHaveBeenCalledWith({}); + expect(onStart).toHaveBeenCalledTimes(3); + expect(onStart).toHaveBeenNthCalledWith(1, 'approveTxHash'); + expect(onStart).toHaveBeenNthCalledWith(2, 'permit2TxHash'); + expect(onStart).toHaveBeenNthCalledWith(3, 'txHash'); }); }); From ab404d8eca2957b6b028ca6b33898a3be7d36f1c Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:51:14 -0700 Subject: [PATCH 05/10] comment --- src/swap/utils/processSwapTransaction.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/swap/utils/processSwapTransaction.ts b/src/swap/utils/processSwapTransaction.ts index 2767d11ddc..d1282280bc 100644 --- a/src/swap/utils/processSwapTransaction.ts +++ b/src/swap/utils/processSwapTransaction.ts @@ -35,6 +35,8 @@ export async function processSwapTransaction({ // for swaps from ERC-20 tokens, // if there is an approveTransaction present, // request approval for the amount + // for V1 API, `approveTx` will be an ERC-20 approval against the Router + // for V2 API, `approveTx` will be an ERC-20 approval against the `Permit2` contract if (approveTransaction?.data) { setPendingTransaction(true); const approveTxHash = await sendTransactionAsync({ @@ -49,10 +51,10 @@ export async function processSwapTransaction({ }); setPendingTransaction(false); - // for the V2 API, we use Uniswap's UniversalRouter + // for the V2 API, we use Uniswap's `UniversalRouter`, which uses `Permit2` for ERC-20 approvals // this adds an additional transaction/step to the swap process - // the `approveTx` on the response will be an approval for the amount of the `from` token against `Permit2`, instead of an approval against the Router itself - // we also need to make an extra transaction to `Permit2` to approve the UniversalRouter to spend the funds + // since we need to make an extra transaction to `Permit2` to allow the UniversalRouter to spend the approved funds + // this would typically be a (gasless) signature, but we're using a transaction here to allow batching for Smart Wallets // read more: https://blog.uniswap.org/permit2-and-universal-router if (!useAggregator) { setPendingTransaction(true); From 69d2ff30afea260e72607dbc71a19a38f2a2c92f Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:51:35 -0700 Subject: [PATCH 06/10] yarn check --- src/swap/utils/processSwapTransaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/swap/utils/processSwapTransaction.ts b/src/swap/utils/processSwapTransaction.ts index d1282280bc..20fc182a34 100644 --- a/src/swap/utils/processSwapTransaction.ts +++ b/src/swap/utils/processSwapTransaction.ts @@ -1,13 +1,13 @@ import type { Address, TransactionReceipt } from 'viem'; +import { encodeFunctionData, parseAbi } from 'viem'; import type { Config } from 'wagmi'; import { waitForTransactionReceipt } from 'wagmi/actions'; import type { SendTransactionMutateAsync } from 'wagmi/query'; import { - UNIVERSALROUTER_CONTRACT_ADDRESS, PERMIT2_CONTRACT_ADDRESS, + UNIVERSALROUTER_CONTRACT_ADDRESS, } from '../constants'; import type { BuildSwapTransaction } from '../types'; -import { encodeFunctionData, parseAbi } from 'viem'; export async function processSwapTransaction({ swapTransaction, From dc5ef93d5d1a9b41a0bc63753a426212163e3137 Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:57:45 -0700 Subject: [PATCH 07/10] changeset --- .changeset/five-berries-warn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/five-berries-warn.md diff --git a/.changeset/five-berries-warn.md b/.changeset/five-berries-warn.md new file mode 100644 index 0000000000..c11363d9ed --- /dev/null +++ b/.changeset/five-berries-warn.md @@ -0,0 +1,5 @@ +--- +"@coinbase/onchainkit": minor +--- + +**feat**: add Permit2 approval process for UniversalRouter by @0xAlec #980 From 7f77855b0310edfa56b2a15e94a750429c619304 Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:24:56 -0700 Subject: [PATCH 08/10] sort constants alphabetically --- src/swap/constants.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/swap/constants.ts b/src/swap/constants.ts index b7c6913ee3..af8bc6fd55 100644 --- a/src/swap/constants.ts +++ b/src/swap/constants.ts @@ -2,11 +2,11 @@ export const GENERAL_SWAP_ERROR_CODE = 'SWAP_ERROR'; export const GENERAL_SWAP_QUOTE_ERROR_CODE = 'SWAP_QUOTE_ERROR'; export const GENERAL_SWAP_BALANCE_ERROR_CODE = 'SWAP_BALANCE_ERROR'; export const LOW_LIQUIDITY_ERROR_CODE = 'SWAP_QUOTE_LOW_LIQUIDITY_ERROR'; +export const PERMIT2_CONTRACT_ADDRESS = + '0x000000000022D473030F116dDEE9F6B43aC78BA3'; export const TOO_MANY_REQUESTS_ERROR_CODE = 'TOO_MANY_REQUESTS_ERROR'; export const UNCAUGHT_SWAP_QUOTE_ERROR_CODE = 'UNCAUGHT_SWAP_QUOTE_ERROR'; export const UNCAUGHT_SWAP_ERROR_CODE = 'UNCAUGHT_SWAP_ERROR'; -export const USER_REJECTED_ERROR_CODE = 'USER_REJECTED'; -export const PERMIT2_CONTRACT_ADDRESS = - '0x000000000022D473030F116dDEE9F6B43aC78BA3'; export const UNIVERSALROUTER_CONTRACT_ADDRESS = '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD'; +export const USER_REJECTED_ERROR_CODE = 'USER_REJECTED'; From 94d9a445a53fd6f5bbb1c06d8fa7ad6db2b1ebe6 Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:31:27 -0700 Subject: [PATCH 09/10] `patch` changeset --- .changeset/five-berries-warn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/five-berries-warn.md b/.changeset/five-berries-warn.md index c11363d9ed..08f1ba4f03 100644 --- a/.changeset/five-berries-warn.md +++ b/.changeset/five-berries-warn.md @@ -1,5 +1,5 @@ --- -"@coinbase/onchainkit": minor +"@coinbase/onchainkit": patch --- **feat**: add Permit2 approval process for UniversalRouter by @0xAlec #980 From 94ccc3b3b9b92f3abb57e8b5841734ce35e9fa9a Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:32:58 -0700 Subject: [PATCH 10/10] add comment for `deadline` --- src/swap/utils/processSwapTransaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/swap/utils/processSwapTransaction.ts b/src/swap/utils/processSwapTransaction.ts index 20fc182a34..fce440ca66 100644 --- a/src/swap/utils/processSwapTransaction.ts +++ b/src/swap/utils/processSwapTransaction.ts @@ -68,7 +68,7 @@ export async function processSwapTransaction({ quote.from.address as Address, UNIVERSALROUTER_CONTRACT_ADDRESS, BigInt(quote.fromAmount), - 20_000_000_000_000, + 20_000_000_000_000, // The deadline where the approval is no longer valid - see https://docs.uniswap.org/contracts/permit2/reference/allowance-transfer ], }); const permitTxnHash = await sendTransactionAsync({