87cbd1ab38
Fixes the swap flow for users who are still in expert mode and need permit2 approvals for a token ### test plan added e2e test for full permit2 flow with expert mode enabled. failing test without the change: <img width="1394" alt="Screenshot 2023-05-31 at 2 24 12 PM" src="https://github.com/Uniswap/interface/assets/66155195/6a39e039-31b5-4bce-91d2-5e3ebc777378"> passing test with change in the CI of this PR
245 lines
9.4 KiB
TypeScript
245 lines
9.4 KiB
TypeScript
import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk'
|
|
|
|
import { DAI, USDC_MAINNET } from '../../src/constants/tokens'
|
|
import { getTestSelector } from '../utils'
|
|
|
|
/** Initiates a swap. */
|
|
function initiateSwap() {
|
|
// The swap button is re-rendered once enable, so we must wait until the original button is not disabled to re-select the appropriate button.
|
|
cy.get('#swap-button').should('not.be.disabled')
|
|
// Completes the swap.
|
|
cy.get('#swap-button').click()
|
|
cy.get(getTestSelector('confirm-swap-button')).click()
|
|
}
|
|
|
|
describe('Permit2', () => {
|
|
// The same tokens & swap-amount combination is used for all permit2 tests.
|
|
const INPUT_TOKEN = DAI
|
|
const OUTPUT_TOKEN = USDC_MAINNET
|
|
const TEST_BALANCE_INCREMENT = 0.01
|
|
|
|
beforeEach(() => {
|
|
// Sets up a swap between INPUT_TOKEN and OUTPUT_TOKEN.
|
|
cy.visit(`/swap/?inputCurrency=${INPUT_TOKEN.address}&outputCurrency=${OUTPUT_TOKEN.address}`, {
|
|
ethereum: 'hardhat',
|
|
})
|
|
cy.get('#swap-currency-input .token-amount-input').type(TEST_BALANCE_INCREMENT.toString())
|
|
})
|
|
|
|
/** Asserts permit2 has a max approval for spend of the input token on-chain. */
|
|
function expectTokenAllowanceForPermit2ToBeMax() {
|
|
// check token approval
|
|
return cy
|
|
.hardhat()
|
|
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }))
|
|
.should('deep.equal', MaxUint256)
|
|
}
|
|
|
|
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
|
|
function expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime: number) {
|
|
return cy
|
|
.hardhat()
|
|
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }))
|
|
.then((allowance) => {
|
|
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.
|
|
const expected = Math.floor((approvalTime + 2_592_000_000) / 1000)
|
|
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
|
|
})
|
|
}
|
|
|
|
it('swaps when user has already approved token and permit2', () => {
|
|
cy.hardhat().then(({ approval, wallet }) => {
|
|
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
|
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
|
})
|
|
initiateSwap()
|
|
cy.get(getTestSelector('confirmation-close-icon')).click()
|
|
// Verifies that there is a successful swap notification.
|
|
cy.contains('Swapped').should('exist')
|
|
})
|
|
|
|
it('swaps after completing full permit2 approval process', () => {
|
|
cy.hardhat().then(({ provider }) => {
|
|
cy.spy(provider, 'send').as('permitApprovalSpy')
|
|
})
|
|
initiateSwap()
|
|
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
|
cy.contains('Approved').should('exist')
|
|
|
|
cy.contains('Allow DAI to be used for swapping').should('exist')
|
|
cy.contains('Confirm Swap').should('exist')
|
|
|
|
cy.then(() => {
|
|
const approvalTime = Date.now()
|
|
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectTokenAllowanceForPermit2ToBeMax()
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
|
})
|
|
})
|
|
|
|
it('swaps after completing full permit2 approval process in expert mode', () => {
|
|
// Sets up a swap between INPUT_TOKEN and OUTPUT_TOKEN.
|
|
cy.visit(`/swap/?inputCurrency=${INPUT_TOKEN.address}&outputCurrency=${OUTPUT_TOKEN.address}&ape=true`, {
|
|
ethereum: 'hardhat',
|
|
})
|
|
cy.get('#swap-currency-input .token-amount-input').type(TEST_BALANCE_INCREMENT.toString())
|
|
|
|
cy.hardhat().then(({ provider }) => {
|
|
cy.spy(provider, 'send').as('permitApprovalSpy')
|
|
})
|
|
initiateSwap()
|
|
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
|
cy.contains('Approved').should('exist')
|
|
|
|
cy.contains('Allow DAI to be used for swapping').should('exist')
|
|
cy.contains('Confirm Swap').should('exist')
|
|
|
|
cy.then(() => {
|
|
const approvalTime = Date.now()
|
|
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectTokenAllowanceForPermit2ToBeMax()
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
|
})
|
|
})
|
|
|
|
it('swaps after handling user rejection of both approval and signature', () => {
|
|
const USER_REJECTION = { code: 4001 }
|
|
cy.hardhat().then((hardhat) => {
|
|
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction')
|
|
tokenApprovalStub.rejects(USER_REJECTION) // reject token approval
|
|
const permitApprovalStub = cy.stub(hardhat.provider, 'send')
|
|
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // reject permit approval
|
|
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
|
|
|
|
initiateSwap()
|
|
|
|
// tokenApprovalStub should reject here, and the modal should revert to the review state.
|
|
cy.contains('Review swap').should('be.visible')
|
|
|
|
cy.then(() => {
|
|
// The user is now allowing approval, but the permit2 signature will be rejected by the user (permitApprovalStub).
|
|
tokenApprovalStub.restore() // allow token approval
|
|
})
|
|
|
|
cy.get(getTestSelector('confirm-swap-button')).click()
|
|
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
|
cy.contains('Approved').should('exist')
|
|
|
|
// permitApprovalStub should reject here, and the modal should revert to the review state.
|
|
cy.contains('Review swap')
|
|
.should('be.visible')
|
|
.then(() => {
|
|
permitApprovalStub.restore() // allow permit approval
|
|
})
|
|
|
|
cy.get(getTestSelector('confirm-swap-button')).click()
|
|
|
|
// The swap should now be able to proceed, as the permit2 signature will be accepted by the user.
|
|
const approvalTime = Date.now()
|
|
|
|
cy.contains('Confirm Swap').should('exist')
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectTokenAllowanceForPermit2ToBeMax()
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
})
|
|
})
|
|
|
|
it('swaps with existing token approval and missing permit approval', () => {
|
|
cy.hardhat().then(({ approval, wallet, provider }) => {
|
|
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
|
cy.spy(provider, 'send').as('permitApprovalSpy')
|
|
})
|
|
cy.then(() => initiateSwap())
|
|
cy.then(() => {
|
|
const approvalTime = Date.now()
|
|
|
|
cy.contains('Confirm Swap').should('exist')
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
|
})
|
|
})
|
|
|
|
it('swaps with existing permit approval and missing token approval', () => {
|
|
cy.hardhat().then(({ approval, wallet }) => approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }))
|
|
cy.then(() => {
|
|
initiateSwap()
|
|
})
|
|
cy.then(() => {
|
|
const approvalTime = Date.now()
|
|
|
|
cy.contains('Confirm Swap').should('exist')
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
})
|
|
})
|
|
|
|
it('prompts signature when existing permit approval is expired', () => {
|
|
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
|
|
|
|
cy.hardhat().then(({ approval, wallet, provider }) => {
|
|
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
|
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, expiredAllowance)
|
|
cy.spy(provider, 'send').as('permitApprovalSpy')
|
|
})
|
|
cy.then(() => {
|
|
initiateSwap()
|
|
})
|
|
cy.then(() => {
|
|
const approvalTime = Date.now()
|
|
|
|
cy.contains('Confirm Swap').should('exist')
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
|
})
|
|
})
|
|
|
|
it('prompts signature when existing permit approval amount is too low', () => {
|
|
const smallAllowance = { amount: 1 }
|
|
|
|
cy.hardhat().then(({ approval, wallet, provider }) => {
|
|
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
|
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, smallAllowance)
|
|
cy.spy(provider, 'send').as('permitApprovalSpy')
|
|
initiateSwap()
|
|
const approvalTime = Date.now()
|
|
|
|
cy.contains('Confirm Swap').should('exist')
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
|
})
|
|
})
|
|
|
|
it('prompts token approval when existing approval amount is too low', () => {
|
|
cy.hardhat()
|
|
.then(({ approval, wallet }) => {
|
|
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
|
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }, 1)
|
|
})
|
|
.then(() => {
|
|
initiateSwap()
|
|
const approvalTime = Date.now()
|
|
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
|
|
|
cy.contains('Confirm Swap').should('exist')
|
|
cy.contains('Swapped').should('exist')
|
|
|
|
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
|
})
|
|
})
|
|
})
|