From 9ba1a11639e6842298d67acca56a1a144d03b7b7 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Fri, 9 Jun 2023 15:50:11 -0400 Subject: [PATCH] feat: add fills to integration tests to test filled, cancelled status (#206) * Before adding hardhat, mocha, and headaches * Add skipLibCheck = true in tsconfig * Get order status running on fork rpc, and basic integ test setup * remove hardhat related imports and update test * Remove hardhat config * Fix code style issues with Prettier * bump gouda-sdk * save state * Fix code style issues with Prettier * bump jest timeout * Working integ tests * Fix code style issues with Prettier * Add other tests * Fix code style issues with Prettier * create random wallet and override storage * Fix code style issues with Prettier * Remove TENDERLY from supported chains * Fix code style issues with Prettier * Add one more test * unskip nonce test * Update readme * Remove comments * Fix code style issues with Prettier * fix unit test * Fix code style issues with Prettier * Add advanced order query system and fix expiry * Fix code style issues with Prettier * riley comments * Fix code style issues with Prettier * minor tweaks * Add tenderly fork creation + deletion * Remove fork creation * Add logging * Add logic to use rpc tenderlyx * Do not dump entire abi of quoter into logs * Pass stage into post order handler * Do not log entire orderChainVerifier abis * Add typing for stage conditional logic * save state * Pass all integ tests * remove comments + fix:prettier * fix readme, and other small things * remove conditional logic for RPC_TENDERLY, and add back sentinal value * Change chainId to TENDERLY in integ tests * Bump gouda-sdk to 0.9.6 * Save state, refactored for 0.9.6 but will rollback * bump gouda-sdk to 1.0.0-alpha.1 * save state * bump gouda-sdk to latest version * remove goerli support * Remove depreacted delete func * pass integ tests? * fix failing unit tests from gouda sdk renaming * fix readme * Add basic fillOrder helper * oops add permit2 approve and pass fill contract test * Add fill test for erc20 - eth * add test for cancelled status for nonce reuse * clean up tests * save state * save state * Remove lastBlock reassignment and replace with startingBlock * replace other uses of lastBlockNumber * Fix COS handler tests * fix prettier * add block checkpoint expect * Add block advance logic to deal with lookback and provider errors * Passes all tests, using block advance at beginning * fix comments * Fix flaky expiry test --------- Co-authored-by: Lint Action Co-authored-by: ConjunctiveNormalForm --- package.json | 2 +- test/abis/permit2.json | 413 +++++++++++++++++++++++++++++++++++++++ test/integ/constants.ts | 1 + test/integ/nonce.test.ts | 2 +- test/integ/order.test.ts | 333 +++++++++++++++++++++++++------ 5 files changed, 685 insertions(+), 66 deletions(-) create mode 100644 test/abis/permit2.json diff --git a/package.json b/package.json index 049c08bc..d1322f4c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "clean": "rm -rf dist cdk.out", "watch": "tsc -w", "test": "jest --detectOpenHandles --forceExit --testPathIgnorePatterns=integ/ dist/", - "integ-test": "jest --detectOpenHandles --testPathPattern=integ/", + "integ-test": "jest --detectOpenHandles --runInBand --testPathPattern=integ/", "cdk": "cdk", "fix": "run-s fix:*", "fix:prettier": "prettier \"lib/**/*.ts\" --write", diff --git a/test/abis/permit2.json b/test/abis/permit2.json new file mode 100644 index 00000000..066c52da --- /dev/null +++ b/test/abis/permit2.json @@ -0,0 +1,413 @@ +{ + "abi": [ + { + "inputs": [{ "internalType": "uint256", "name": "deadline", "type": "uint256" }], + "name": "AllowanceExpired", + "type": "error" + }, + { "inputs": [], "name": "ExcessiveInvalidation", "type": "error" }, + { + "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], + "name": "InsufficientAllowance", + "type": "error" + }, + { + "inputs": [{ "internalType": "uint256", "name": "maxAmount", "type": "uint256" }], + "name": "InvalidAmount", + "type": "error" + }, + { "inputs": [], "name": "InvalidContractSignature", "type": "error" }, + { "inputs": [], "name": "InvalidNonce", "type": "error" }, + { "inputs": [], "name": "InvalidSignature", "type": "error" }, + { "inputs": [], "name": "InvalidSignatureLength", "type": "error" }, + { "inputs": [], "name": "InvalidSigner", "type": "error" }, + { "inputs": [], "name": "LengthMismatch", "type": "error" }, + { + "inputs": [{ "internalType": "uint256", "name": "signatureDeadline", "type": "uint256" }], + "name": "SignatureExpired", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "token", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "indexed": false, "internalType": "uint48", "name": "expiration", "type": "uint48" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "token", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "Lockdown", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "token", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint48", "name": "newNonce", "type": "uint48" }, + { "indexed": false, "internalType": "uint48", "name": "oldNonce", "type": "uint48" } + ], + "name": "NonceInvalidation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "token", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "indexed": false, "internalType": "uint48", "name": "expiration", "type": "uint48" }, + { "indexed": false, "internalType": "uint48", "name": "nonce", "type": "uint48" } + ], + "name": "Permit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "word", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "mask", "type": "uint256" } + ], + "name": "UnorderedNonceInvalidation", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "allowance", + "outputs": [ + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "uint48", "name": "expiration", "type": "uint48" }, + { "internalType": "uint48", "name": "nonce", "type": "uint48" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "uint48", "name": "expiration", "type": "uint48" } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint48", "name": "newNonce", "type": "uint48" } + ], + "name": "invalidateNonces", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "wordPos", "type": "uint256" }, + { "internalType": "uint256", "name": "mask", "type": "uint256" } + ], + "name": "invalidateUnorderedNonces", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "internalType": "struct IAllowanceTransfer.TokenSpenderPair[]", + "name": "approvals", + "type": "tuple[]" + } + ], + "name": "lockdown", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "nonceBitmap", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "uint48", "name": "expiration", "type": "uint48" }, + { "internalType": "uint48", "name": "nonce", "type": "uint48" } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails[]", + "name": "details", + "type": "tuple[]" + }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "sigDeadline", "type": "uint256" } + ], + "internalType": "struct IAllowanceTransfer.PermitBatch", + "name": "permitBatch", + "type": "tuple" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "uint48", "name": "expiration", "type": "uint48" }, + { "internalType": "uint48", "name": "nonce", "type": "uint48" } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails", + "name": "details", + "type": "tuple" + }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "sigDeadline", "type": "uint256" } + ], + "internalType": "struct IAllowanceTransfer.PermitSingle", + "name": "permitSingle", + "type": "tuple" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions", + "name": "permitted", + "type": "tuple" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "requestedAmount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails", + "name": "transferDetails", + "type": "tuple" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "name": "permitted", + "type": "tuple[]" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "requestedAmount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions", + "name": "permitted", + "type": "tuple" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "requestedAmount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails", + "name": "transferDetails", + "type": "tuple" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes32", "name": "witness", "type": "bytes32" }, + { "internalType": "string", "name": "witnessTypeString", "type": "string" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitWitnessTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "name": "permitted", + "type": "tuple[]" + }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "requestedAmount", "type": "uint256" } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + }, + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "bytes32", "name": "witness", "type": "bytes32" }, + { "internalType": "string", "name": "witnessTypeString", "type": "string" }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "name": "permitWitnessTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "address", "name": "token", "type": "address" } + ], + "internalType": "struct IAllowanceTransfer.AllowanceTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint160", "name": "amount", "type": "uint160" }, + { "internalType": "address", "name": "token", "type": "address" } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} diff --git a/test/integ/constants.ts b/test/integ/constants.ts index dbb44026..32181fc2 100644 --- a/test/integ/constants.ts +++ b/test/integ/constants.ts @@ -1,5 +1,6 @@ export const ANVIL_TEST_WALLET_PK = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +export const PERMIT2 = '0x000000000022d473030f116ddee9f6b43ac78ba3' export const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' export const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' diff --git a/test/integ/nonce.test.ts b/test/integ/nonce.test.ts index f31af644..4269b240 100644 --- a/test/integ/nonce.test.ts +++ b/test/integ/nonce.test.ts @@ -13,7 +13,7 @@ const amount = BigNumber.from(10).pow(18) axios.defaults.timeout = 10000 -describe('get nonce', () => { +xdescribe('get nonce', () => { it('should get current nonce for address, and increment it by one after the address posts an order', async () => { const address = (await wallet.getAddress()).toLowerCase() const getResponse = await axios.get(`${URL}dutch-auction/nonce?address=${address}`) diff --git a/test/integ/order.test.ts b/test/integ/order.test.ts index f74f9f5d..60b3a546 100644 --- a/test/integ/order.test.ts +++ b/test/integ/order.test.ts @@ -1,21 +1,47 @@ -import { DutchOrderBuilder } from '@uniswap/gouda-sdk' +import { DutchOrder, DutchOrderBuilder, REACTOR_ADDRESS_MAPPING, SignedOrder } from '@uniswap/gouda-sdk' +import { factories } from '@uniswap/gouda-sdk/dist/src/contracts/index' import axios from 'axios' import dotenv from 'dotenv' import { BigNumber, Contract, ethers, Wallet } from 'ethers' -import { UNI, WETH, ZERO_ADDRESS } from './constants' +import { PERMIT2, UNI, WETH, ZERO_ADDRESS } from './constants' + +const { DutchLimitOrderReactor__factory } = factories import { GetOrdersResponse } from '../../lib/handlers/get-orders/schema' import { ChainId } from '../../lib/util/chain' import * as ERC20_ABI from '../abis/erc20.json' +import * as PERMIT2_ABI from '../abis/permit2.json' +import { FILL_EVENT_LOOKBACK_BLOCKS_ON } from '../../lib/handlers/check-order-status/handler' const { abi } = ERC20_ABI +const { abi: permit2Abi } = PERMIT2_ABI dotenv.config() -const PERMIT2 = '0x000000000022d473030f116ddee9f6b43ac78ba3' +type OrderExecution = { + orders: SignedOrder[] + reactor: string + fillContract: string + fillData: string +} + +// if the CLI argument runInBand is not provided, throw +if (!process.argv.includes('--runInBand')) { + throw new Error('Integration tests must be run with --runInBand flag') +} + +// Some reason this fails with + 1, TODO: +const FILL_EVENT_BLOCK_OFFSET = FILL_EVENT_LOOKBACK_BLOCKS_ON(ChainId.TENDERLY) + 5 +// This needs to be greater than the sum of all fill event block offsets used +// i.e. if we have 10 tests, we need to advance the block number before running any tests by at least 10 * FILL_EVENT_BLOCK_OFFSET +// in the beginning of the test suite, or else the sfn will pick up old fill events that are no longer on the chain +// If you start getting errors about orders that are supposed to be filled being open, increase this number +const INTIAL_BLOCK_OFFSET = 200 describe('/dutch-auction/order', () => { - jest.setTimeout(60 * 1000) - let wallet: Wallet + const DEFAULT_DEADLINE_SECONDS = 500 + jest.setTimeout(90 * 1000) + let alice: Wallet + let filler: Wallet let provider: ethers.providers.JsonRpcProvider let aliceAddress: string let nonce: BigNumber @@ -25,6 +51,8 @@ describe('/dutch-auction/order', () => { let uni: Contract // Fork management let snap: null | string = null + let checkpointedBlock: ethers.providers.Block + let blockOffsetCounter: number = 0 beforeAll(async () => { if (!process.env.GOUDA_SERVICE_URL) { @@ -36,46 +64,76 @@ describe('/dutch-auction/order', () => { URL = process.env.GOUDA_SERVICE_URL provider = new ethers.providers.JsonRpcProvider(process.env.RPC_12341234) + // advance blocks to avoid mixing fill events with previous test runs + const startingBlockNumber = (await provider.getBlock('latest')).number + await provider.send('evm_increaseBlocks', [ethers.utils.hexValue( + INTIAL_BLOCK_OFFSET + )]) + expect((await provider.getBlock('latest')).number).toEqual(startingBlockNumber + INTIAL_BLOCK_OFFSET) - wallet = ethers.Wallet.createRandom().connect(provider) - aliceAddress = (await wallet.getAddress()).toLowerCase() + alice = ethers.Wallet.createRandom().connect(provider) + filler = ethers.Wallet.createRandom().connect(provider) + aliceAddress = (await alice.getAddress()).toLowerCase() weth = new Contract(WETH, abi, provider) uni = new Contract(UNI, abi, provider) + const permit2Contract = new Contract(PERMIT2, permit2Abi, provider) - // Set alice's balance to 10 ETH - await provider.send('tenderly_setBalance', [ - [aliceAddress], - ethers.utils.hexValue(ethers.utils.parseUnits('10', 'ether').toHexString()), - ]) - - // Ensure alice has some WETH and UNI - await provider.send('tenderly_setStorageAt', [ - UNI, - ethers.utils.keccak256( - ethers.utils.concat([ - ethers.utils.hexZeroPad(aliceAddress, 32), - ethers.utils.hexZeroPad('0x04', 32), // the balances slot is 4th in the UNI contract + const fundWallets = async (wallets: Wallet[]) => { + for (const wallet of wallets) { + await provider.send('tenderly_setBalance', [ + [wallet.address], + ethers.utils.hexValue(ethers.utils.parseUnits('10', 'ether').toHexString()), + ]) + expect(await provider.getBalance(wallet.address)).toEqual(ethers.utils.parseEther('10')) + // Ensure both alice and filler have WETH and UNI + await provider.send('tenderly_setStorageAt', [ + UNI, + ethers.utils.keccak256( + ethers.utils.concat([ + ethers.utils.hexZeroPad(wallet.address, 32), + ethers.utils.hexZeroPad('0x04', 32), // the balances slot is 4th in the UNI contract + ]) + ), + ethers.utils.hexZeroPad(ethers.utils.parseEther('20').toHexString(), 32), ]) - ), - ethers.utils.hexZeroPad(ethers.utils.parseEther('20').toHexString(), 32), - ]) - const uniBalance = (await uni.balanceOf(wallet.address)) as BigNumber - expect(uniBalance).toEqual(ethers.utils.parseEther('20')) - - await provider.send('tenderly_setStorageAt', [ - WETH, - ethers.utils.keccak256( - ethers.utils.concat([ethers.utils.hexZeroPad(aliceAddress, 32), ethers.utils.hexZeroPad('0x03', 32)]) - ), - ethers.utils.hexZeroPad(ethers.utils.parseEther('20').toHexString(), 32), - ]) - const wethBalance = (await weth.balanceOf(wallet.address)) as BigNumber - expect(wethBalance).toEqual(ethers.utils.parseEther('20')) - - // approve P2 - await weth.connect(wallet).approve(PERMIT2, ethers.constants.MaxUint256) - await uni.connect(wallet).approve(PERMIT2, ethers.constants.MaxUint256) + const uniBalance = (await uni.balanceOf(wallet.address)) as BigNumber + expect(uniBalance).toEqual(ethers.utils.parseEther('20')) + + await provider.send('tenderly_setStorageAt', [ + WETH, + ethers.utils.keccak256( + ethers.utils.concat([ethers.utils.hexZeroPad(wallet.address, 32), ethers.utils.hexZeroPad('0x03', 32)]) + ), + ethers.utils.hexZeroPad(ethers.utils.parseEther('20').toHexString(), 32), + ]) + const wethBalance = (await weth.balanceOf(wallet.address)) as BigNumber + expect(wethBalance).toEqual(ethers.utils.parseEther('20')) + + // approve P2 + await weth.connect(wallet).approve(PERMIT2, ethers.constants.MaxUint256) + await uni.connect(wallet).approve(PERMIT2, ethers.constants.MaxUint256) + // approve reactor for permit2 + await permit2Contract + .connect(wallet) + .approve( + weth.address, + REACTOR_ADDRESS_MAPPING[ChainId.MAINNET]['Dutch'], + ethers.utils.parseEther('100'), + 281474976710655 + ) + await permit2Contract + .connect(wallet) + .approve( + uni.address, + REACTOR_ADDRESS_MAPPING[ChainId.MAINNET]['Dutch'], + ethers.utils.parseEther('100'), + 281474976710655 + ) + } + } + + await fundWallets([alice, filler]) const getResponse = await axios.get(`${URL}dutch-auction/nonce?address=${aliceAddress}`) expect(getResponse.status).toEqual(200) @@ -83,12 +141,18 @@ describe('/dutch-auction/order', () => { expect(nonce.lt(ethers.constants.MaxUint256)).toBeTruthy() }) + afterAll(async () => { + // Do not revert to test slate, so next run should be roughly 200 blocks ahead of the previous run + }) + beforeEach(async () => { + checkpointedBlock = await provider.getBlock('latest') snap = await provider.send('evm_snapshot', []) }) afterEach(async () => { await provider.send('evm_revert', [snap]) + expect(await provider.getBlock('latest')).toEqual(checkpointedBlock) }) async function expectOrdersToBeOpen(orderHashes: string[]) { @@ -111,21 +175,22 @@ describe('/dutch-auction/order', () => { } async function waitAndGetOrderStatus(orderHash: string, deadlineSeconds: number) { - /// @dev testing expiry of the order via the step function is very finicky + /// @dev testing order status updates via the step function is very finicky /// we fast forward the fork's timestamp by the deadline and then mine a block to get the changes included /// However, we have to wait for the sfn to fire again, so we wait a bit, and as long as the order's expiry is longer than that time period, /// we can be sure that the order correctly expired based on the block.timestamp const params = [ ethers.utils.hexValue(deadlineSeconds), // hex encoded number of seconds ] - const blockNumber = (await provider.getBlock('latest')).number - await provider.send('evm_increaseTime', params) + const blockNumber = (await provider.getBlock('latest')).number const blocksToMine = 1 await provider.send('evm_increaseBlocks', [ethers.utils.hexValue(blocksToMine)]) - expect((await provider.getBlock('latest')).number).toEqual(blockNumber + blocksToMine + 1) + expect((await provider.getBlock('latest')).number).toEqual(blockNumber + blocksToMine) // Wait a bit for sfn to fire again - await new Promise((resolve) => setTimeout(resolve, 15_000)) + // The next retry is usually in 12 seconds but can take longer to complete + // If you start getting errors about orders that are supposed to be expired being open, increase this number + await new Promise((resolve) => setTimeout(resolve, 20_000)) const resp = await axios.get(`${URL}dutch-auction/orders?orderHash=${orderHash}`) expect(resp.status).toEqual(200) @@ -142,7 +207,10 @@ describe('/dutch-auction/order', () => { deadlineSeconds: number, inputToken: string, outputToken: string - ) => { + ): Promise<{ + order: DutchOrder + signature: string + }> => { const deadline = Math.round(new Date().getTime() / 1000) + deadlineSeconds const startTime = Math.round(new Date().getTime() / 1000) const nextNonce = nonce.add(1) @@ -166,7 +234,7 @@ describe('/dutch-auction/order', () => { .build() const { domain, types, values } = order.permitData() - const signature = await wallet._signTypedData(domain, types, values) + const signature = await alice._signTypedData(domain, types, values) const encodedOrder = order.serialize() try { @@ -184,47 +252,184 @@ describe('/dutch-auction/order', () => { }, } ) - expect(postResponse.status).toEqual(201) const newGetResponse = await axios.get(`${URL}dutch-auction/nonce?address=${aliceAddress}`) expect(newGetResponse.status).toEqual(200) const newNonce = BigNumber.from(newGetResponse.data.nonce) expect(newNonce.eq(nonce.add(1))).toBeTruthy() - - return postResponse.data.hash + return { order, signature } } catch (err: any) { console.log(err) throw err } } - xdescribe('checking expiry', () => { + const fillOrder = async (order: DutchOrder, signature: string) => { + const execution: OrderExecution = { + orders: [ + { + order, + signature, + }, + ], + reactor: REACTOR_ADDRESS_MAPPING[ChainId.MAINNET]['Dutch'], + // direct fill is 0x01 + fillContract: '0x0000000000000000000000000000000000000001', + fillData: '0x', + } + + // if output token is ETH, then the value is the amount of ETH to send + const value = order.info.outputs[0].token == ZERO_ADDRESS ? order.info.outputs[0].startAmount : 0 + + const reactor = DutchLimitOrderReactor__factory.connect(execution.reactor, provider) + const fillerNonce = await filler.getTransactionCount() + const maxFeePerGas = (await provider.getFeeData()).maxFeePerGas + + const populatedTx = await reactor.populateTransaction.executeBatch( + execution.orders.map((order) => { + return { + order: order.order.serialize(), + sig: order.signature, + } + }), + execution.fillContract, + execution.fillData, + { + gasLimit: BigNumber.from(700_000), + nonce: fillerNonce, + ...(maxFeePerGas && { maxFeePerGas }), + maxPriorityFeePerGas: ethers.utils.parseUnits('50', 'gwei'), + value, + } + ) + + populatedTx.gasLimit = BigNumber.from(700_000) + + const tx = await filler.sendTransaction(populatedTx) + const receipt = await tx.wait() + return receipt.transactionHash + } + + describe('checking expiry', () => { it('erc20 to erc20', async () => { const amount = ethers.utils.parseEther('1') - const orderHash = await buildAndSubmitOrder(aliceAddress, amount, 1000, WETH, UNI) - expect(await expectOrdersToBeOpen([orderHash])).toBeTruthy() - expect(await waitAndGetOrderStatus(orderHash, 1001)).toBe('expired') + const { order } = await buildAndSubmitOrder(aliceAddress, amount, DEFAULT_DEADLINE_SECONDS, WETH, UNI) + expect(await expectOrdersToBeOpen([order.hash()])).toBeTruthy() + expect(await waitAndGetOrderStatus(order.hash(), DEFAULT_DEADLINE_SECONDS + 1)).toBe('expired') }) it('erc20 to eth', async () => { const amount = ethers.utils.parseEther('1') - const orderHash = await buildAndSubmitOrder(aliceAddress, amount, 1000, UNI, ZERO_ADDRESS) - expect(await expectOrdersToBeOpen([orderHash])).toBeTruthy() - expect(await waitAndGetOrderStatus(orderHash, 1001)).toBe('expired') + const { order } = await buildAndSubmitOrder(aliceAddress, amount, DEFAULT_DEADLINE_SECONDS, UNI, ZERO_ADDRESS) + expect(await expectOrdersToBeOpen([order.hash()])).toBeTruthy() + expect(await waitAndGetOrderStatus(order.hash(), DEFAULT_DEADLINE_SECONDS + 1)).toBe('expired') }) it('does not expire order before deadline', async () => { const amount = ethers.utils.parseEther('1') - const orderHash = await buildAndSubmitOrder(aliceAddress, amount, 1000, UNI, ZERO_ADDRESS) - expect(await expectOrdersToBeOpen([orderHash])).toBeTruthy() - expect(await waitAndGetOrderStatus(orderHash, 900)).toBe('open') + const { order } = await buildAndSubmitOrder(aliceAddress, amount, DEFAULT_DEADLINE_SECONDS, UNI, ZERO_ADDRESS) + expect(await expectOrdersToBeOpen([order.hash()])).toBeTruthy() + expect(await waitAndGetOrderStatus(order.hash(), DEFAULT_DEADLINE_SECONDS - 100)).toBe('open') }) }) - it('allows same offerer to post multiple orders', async () => { - const amount = ethers.utils.parseEther('1') - const orderHash1 = await buildAndSubmitOrder(aliceAddress, amount, 5, WETH, UNI) - const orderHash2 = await buildAndSubmitOrder(aliceAddress, amount, 5, UNI, ZERO_ADDRESS) - expect(await expectOrdersToBeOpen([orderHash1, orderHash2])).toBeTruthy() + const advanceBlocks = async (numBlocks: number) => { + if(numBlocks == 0) { + return + } + await provider.send('evm_increaseBlocks', [ethers.utils.hexValue(numBlocks)]) + expect((await provider.getBlock('latest')).number).toEqual(checkpointedBlock.number + numBlocks) + } + + describe('+ attempt to fill', () => { + // The SFN will get fill logs for all orders that were filled in the last 10 blocks + // However, since we are performing a re-org by reverting the chain after every test, + // many of these orders will no longer exist (thus the provider call for the txnHash will fail) + // So, we keep a running total of the offset from the current block number to advance the chain by every time + beforeEach(async () => { + await advanceBlocks(blockOffsetCounter) + }) + + afterEach(async () => { + blockOffsetCounter += FILL_EVENT_BLOCK_OFFSET + }) + + it('erc20 to eth', async () => { + const amount = ethers.utils.parseEther('1') + const { order, signature } = await buildAndSubmitOrder( + aliceAddress, + amount, + DEFAULT_DEADLINE_SECONDS, + UNI, + ZERO_ADDRESS + ) + expect(await expectOrdersToBeOpen([order.hash()])).toBeTruthy() + const txHash = await fillOrder(order, signature) + expect(txHash).toBeDefined() + expect(await waitAndGetOrderStatus(order.hash(), 0)).toBe('filled') + }) + + it('erc20 to erc20', async () => { + const amount = ethers.utils.parseEther('1') + const { order, signature } = await buildAndSubmitOrder(aliceAddress, amount, DEFAULT_DEADLINE_SECONDS, WETH, UNI) + expect(await expectOrdersToBeOpen([order.hash()])).toBeTruthy() + const txHash = await fillOrder(order, signature) + expect(txHash).toBeDefined() + expect(await waitAndGetOrderStatus(order.hash(), 0)).toBe('filled') + }) + + describe('checking cancel', () => { + it('updates status to cancelled when fill reverts due to nonce reuse', async () => { + const amount = ethers.utils.parseEther('1') + const { order: order1, signature: sig1 } = await buildAndSubmitOrder( + aliceAddress, + amount, + DEFAULT_DEADLINE_SECONDS, + WETH, + UNI + ) + const { order: order2, signature: sig2 } = await buildAndSubmitOrder( + aliceAddress, + amount, + DEFAULT_DEADLINE_SECONDS, + UNI, + ZERO_ADDRESS + ) + expect(order1.info.nonce.toString()).toEqual(order2.info.nonce.toString()) + expect(await expectOrdersToBeOpen([order1.hash(), order2.hash()])).toBeTruthy() + // fill the first one + const txHash = await fillOrder(order1, sig1) + expect(txHash).toBeDefined() + expect(await waitAndGetOrderStatus(order1.hash(), 0)).toBe('filled') + // try to fill the second one, expect revert + try { + await fillOrder(order2, sig2) + expect(true).toBeFalsy() + } catch (err: any) { + expect(err.message.includes('transaction failed')).toBeTruthy(); + } + expect(await waitAndGetOrderStatus(order2.hash(), 0)).toBe('cancelled') + }) + + it('allows same offerer to post multiple orders with different nonces and be filled', async () => { + const amount = ethers.utils.parseEther('1') + const { order: order1, signature: sig1 } = await buildAndSubmitOrder(aliceAddress, amount, DEFAULT_DEADLINE_SECONDS, WETH, UNI) + nonce = nonce.add(1) + const { order: order2, signature: sig2 } = await buildAndSubmitOrder( + aliceAddress, + amount, + DEFAULT_DEADLINE_SECONDS, + UNI, + ZERO_ADDRESS + ) + expect(await expectOrdersToBeOpen([order1.hash(), order2.hash()])).toBeTruthy() + const txHash = await fillOrder(order1, sig1) + expect(txHash).toBeDefined() + expect(await waitAndGetOrderStatus(order1.hash(), 0)).toBe('filled') + const txHash2 = await fillOrder(order2, sig2) + expect(txHash2).toBeDefined() + expect(await waitAndGetOrderStatus(order2.hash(), 0)).toBe('filled') + }) + }) }) })