Commit 54b4567a authored by eddie's avatar eddie Committed by GitHub

test: add e2e test for usdt special case (#6784)

* feat: revoke USDT approvals in ConfirmSwapModal

* chore: fix bad merge

* test: add e2e test for usdt special case

* fix: refactor test

* fix: make variable static

* fix: comments
parent fc45a504
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk' import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { DAI, USDC_MAINNET } from '../../src/constants/tokens' import { DAI, USDC_MAINNET, USDT } from '../../src/constants/tokens'
import { getTestSelector } from '../utils' import { getTestSelector } from '../utils'
/** Initiates a swap. */ /** Initiates a swap. */
...@@ -13,30 +15,26 @@ function initiateSwap() { ...@@ -13,30 +15,26 @@ function initiateSwap() {
} }
describe('Permit2', () => { describe('Permit2', () => {
// The same tokens are used for all permit2 tests. function setupInputs(inputToken: Token, outputToken: Token) {
const INPUT_TOKEN = DAI // Sets up a swap between inputToken and outputToken.
const OUTPUT_TOKEN = USDC_MAINNET cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`, {
beforeEach(() => {
// Sets up a swap between INPUT_TOKEN and OUTPUT_TOKEN.
cy.visit(`/swap/?inputCurrency=${INPUT_TOKEN.address}&outputCurrency=${OUTPUT_TOKEN.address}`, {
ethereum: 'hardhat', ethereum: 'hardhat',
}) })
cy.get('#swap-currency-input .token-amount-input').type('0.01') cy.get('#swap-currency-input .token-amount-input').type('0.01')
}) }
/** Asserts permit2 has a max approval for spend of the input token on-chain. */ /** Asserts permit2 has a max approval for spend of the input token on-chain. */
function expectTokenAllowanceForPermit2ToBeMax() { function expectTokenAllowanceForPermit2ToBeMax(inputToken: Token) {
// check token approval // check token approval
cy.hardhat() cy.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })) .then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
.should('deep.equal', MaxUint256) .should('deep.equal', MaxUint256)
} }
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */ /** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
function expectPermit2AllowanceForUniversalRouterToBeMax() { function expectPermit2AllowanceForUniversalRouterToBeMax(inputToken: Token) {
cy.hardhat() cy.hardhat()
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN })) .then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: inputToken }))
.then((allowance) => { .then((allowance) => {
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true) cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds. // Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
...@@ -51,6 +49,7 @@ describe('Permit2', () => { ...@@ -51,6 +49,7 @@ describe('Permit2', () => {
beforeEach(() => cy.hardhat({ automine: false })) beforeEach(() => cy.hardhat({ automine: false }))
it('swaps after completing full permit2 approval process', () => { it('swaps after completing full permit2 approval process', () => {
setupInputs(DAI, USDC_MAINNET)
initiateSwap() initiateSwap()
// verify that the modal retains its state when the window loses focus // verify that the modal retains its state when the window loses focus
...@@ -61,7 +60,7 @@ describe('Permit2', () => { ...@@ -61,7 +60,7 @@ describe('Permit2', () => {
cy.wait('@eth_sendRawTransaction') cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine()) cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('popups')).contains('Approved') cy.get(getTestSelector('popups')).contains('Approved')
expectTokenAllowanceForPermit2ToBeMax() expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify permit2 approval // Verify permit2 approval
cy.contains('Allow DAI to be used for swapping') cy.contains('Allow DAI to be used for swapping')
...@@ -70,12 +69,13 @@ describe('Permit2', () => { ...@@ -70,12 +69,13 @@ describe('Permit2', () => {
cy.hardhat().then((hardhat) => hardhat.mine()) cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Success') cy.contains('Success')
cy.get(getTestSelector('popups')).contains('Swapped') cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax() expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
}) })
it('swaps with existing permit approval and missing token approval', () => { it('swaps with existing permit approval and missing token approval', () => {
setupInputs(DAI, USDC_MAINNET)
cy.hardhat().then(async (hardhat) => { cy.hardhat().then(async (hardhat) => {
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }) await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
await hardhat.mine() await hardhat.mine()
}) })
initiateSwap() initiateSwap()
...@@ -85,7 +85,50 @@ describe('Permit2', () => { ...@@ -85,7 +85,50 @@ describe('Permit2', () => {
cy.wait('@eth_sendRawTransaction') cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine()) cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('popups')).contains('Approved') cy.get(getTestSelector('popups')).contains('Approved')
expectTokenAllowanceForPermit2ToBeMax() expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify transaction
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Success')
cy.get(getTestSelector('popups')).contains('Swapped')
})
/**
* On mainnet, you have to revoke USDT approval before increasing it.
* From the token contract:
* To change the approve amount you first have to reduce the addresses`
* allowance to zero by calling `approve(_spender, 0)` if it is not
* already 0 to mitigate the race condition described here:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*/
it('swaps USDT with existing permit, and existing but insufficient token approval', () => {
cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
await hardhat.mine()
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6)
await hardhat.mine()
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT })
await hardhat.mine()
})
setupInputs(USDT, USDC_MAINNET)
cy.get('#swap-currency-input .token-amount-input').clear().type('2')
initiateSwap()
// Verify allowance revocation
cy.contains('Reset USDT')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: USDT }))
.should('deep.equal', BigNumber.from(0))
// Verify token approval
cy.contains('Enable spending USDT on Uniswap')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('popups')).contains('Approved')
expectTokenAllowanceForPermit2ToBeMax(USDT)
// Verify transaction // Verify transaction
cy.wait('@eth_sendRawTransaction') cy.wait('@eth_sendRawTransaction')
...@@ -98,10 +141,11 @@ describe('Permit2', () => { ...@@ -98,10 +141,11 @@ describe('Permit2', () => {
it('swaps when user has already approved token and permit2', () => { it('swaps when user has already approved token and permit2', () => {
cy.hardhat().then(({ approval, wallet }) => cy.hardhat().then(({ approval, wallet }) =>
Promise.all([ Promise.all([
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }), approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }), approval.setPermit2Allowance({ owner: wallet, token: DAI }),
]) ])
) )
setupInputs(DAI, USDC_MAINNET)
initiateSwap() initiateSwap()
// Verify transaction // Verify transaction
...@@ -110,6 +154,7 @@ describe('Permit2', () => { ...@@ -110,6 +154,7 @@ describe('Permit2', () => {
}) })
it('swaps after handling user rejection of both approval and signature', () => { it('swaps after handling user rejection of both approval and signature', () => {
setupInputs(DAI, USDC_MAINNET)
const USER_REJECTION = { code: 4001 } const USER_REJECTION = { code: 4001 }
cy.hardhat().then((hardhat) => { cy.hardhat().then((hardhat) => {
// Reject token approval // Reject token approval
...@@ -132,7 +177,7 @@ describe('Permit2', () => { ...@@ -132,7 +177,7 @@ describe('Permit2', () => {
// Verify token approval // Verify token approval
cy.get(getTestSelector('popups')).contains('Approved') cy.get(getTestSelector('popups')).contains('Approved')
expectTokenAllowanceForPermit2ToBeMax() expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify permit2 approval rejection // Verify permit2 approval rejection
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4') cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
...@@ -145,30 +190,32 @@ describe('Permit2', () => { ...@@ -145,30 +190,32 @@ describe('Permit2', () => {
// Verify permit2 approval // Verify permit2 approval
cy.contains('Success') cy.contains('Success')
cy.get(getTestSelector('popups')).contains('Swapped') cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax() expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
}) })
}) })
it('prompts token approval when existing approval amount is too low', () => { it('prompts token approval when existing approval amount is too low', () => {
setupInputs(DAI, USDC_MAINNET)
cy.hardhat().then(({ approval, wallet }) => cy.hardhat().then(({ approval, wallet }) =>
Promise.all([ Promise.all([
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }), approval.setPermit2Allowance({ owner: wallet, token: DAI }),
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }, 1), approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }, 1),
]) ])
) )
initiateSwap() initiateSwap()
// Verify token approval // Verify token approval
cy.get(getTestSelector('popups')).contains('Approved') cy.get(getTestSelector('popups')).contains('Approved')
expectPermit2AllowanceForUniversalRouterToBeMax() expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
}) })
it('prompts signature when existing permit approval is expired', () => { it('prompts signature when existing permit approval is expired', () => {
setupInputs(DAI, USDC_MAINNET)
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) } const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
cy.hardhat().then(({ approval, wallet }) => cy.hardhat().then(({ approval, wallet }) =>
Promise.all([ Promise.all([
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }), approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, expiredAllowance), approval.setPermit2Allowance({ owner: wallet, token: DAI }, expiredAllowance),
]) ])
) )
initiateSwap() initiateSwap()
...@@ -177,15 +224,16 @@ describe('Permit2', () => { ...@@ -177,15 +224,16 @@ describe('Permit2', () => {
cy.wait('@eth_signTypedData_v4') cy.wait('@eth_signTypedData_v4')
cy.contains('Success') cy.contains('Success')
cy.get(getTestSelector('popups')).contains('Swapped') cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax() expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
}) })
it('prompts signature when existing permit approval amount is too low', () => { it('prompts signature when existing permit approval amount is too low', () => {
setupInputs(DAI, USDC_MAINNET)
const smallAllowance = { amount: 1 } const smallAllowance = { amount: 1 }
cy.hardhat().then(({ approval, wallet }) => cy.hardhat().then(({ approval, wallet }) =>
Promise.all([ Promise.all([
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }), approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, smallAllowance), approval.setPermit2Allowance({ owner: wallet, token: DAI }, smallAllowance),
]) ])
) )
initiateSwap() initiateSwap()
...@@ -194,6 +242,6 @@ describe('Permit2', () => { ...@@ -194,6 +242,6 @@ describe('Permit2', () => {
cy.wait('@eth_signTypedData_v4') cy.wait('@eth_signTypedData_v4')
cy.contains('Success') cy.contains('Success')
cy.get(getTestSelector('popups')).contains('Swapped') cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax() expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
}) })
}) })
...@@ -7,6 +7,8 @@ import { ApproveTransactionInfo, TransactionType } from 'state/transactions/type ...@@ -7,6 +7,8 @@ import { ApproveTransactionInfo, TransactionType } from 'state/transactions/type
import { UserRejectedRequestError } from 'utils/errors' import { UserRejectedRequestError } from 'utils/errors'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage' import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
const MAX_ALLOWANCE = MaxUint256.toString()
export function useTokenAllowance( export function useTokenAllowance(
token?: Token, token?: Token,
owner?: string, owner?: string,
...@@ -48,8 +50,7 @@ export function useUpdateTokenAllowance( ...@@ -48,8 +50,7 @@ export function useUpdateTokenAllowance(
if (!contract) throw new Error('missing contract') if (!contract) throw new Error('missing contract')
if (!spender) throw new Error('missing spender') if (!spender) throw new Error('missing spender')
const maxAllowance = MaxUint256.toString() const allowance = amount.equalTo(0) ? '0' : MAX_ALLOWANCE
const allowance = amount.equalTo(0) ? '0' : maxAllowance
const response = await contract.approve(spender, allowance) const response = await contract.approve(spender, allowance)
return { return {
response, response,
......
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