From 8c95973ab7ee97daf022a780e5d23c37e0e1246a Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Thu, 20 May 2021 15:32:00 +0200 Subject: [PATCH 01/10] feat: initial stab at Uniswap V3 adaptor --- contracts/mocks/external/ZeroExMock.sol | 36 +++++ .../integration/exchange/ZeroExApiAdapter.sol | 20 ++- package.json | 1 + .../exchange/zeroExApiAdapter.spec.ts | 120 ++++++++++------ yarn.lock | 134 ++++++++++++++++-- 5 files changed, 257 insertions(+), 54 deletions(-) diff --git a/contracts/mocks/external/ZeroExMock.sol b/contracts/mocks/external/ZeroExMock.sol index 89df746a7..bcf9247a1 100644 --- a/contracts/mocks/external/ZeroExMock.sol +++ b/contracts/mocks/external/ZeroExMock.sol @@ -142,6 +142,42 @@ contract ZeroExMock { _transferTokens(); } + function sellEthForTokenToUniswapV3( + bytes memory /* encodedPath */, + uint256 /* minBuyAmount */, + address /* recipient */ + ) + external + payable + returns (uint256) + { + _transferTokens(); + } + + function sellTokenForEthToUniswapV3( + bytes memory /* encodedPath */, + uint256 /* sellAmount */, + uint256 /* minBuyAmount */, + address payable /* recipient */ + ) + external + returns (uint256) + { + _transferTokens(); + } + + function sellTokenForTokenToUniswapV3( + bytes memory /* encodedPath */, + uint256 /* sellAmount */, + uint256 /* minBuyAmount */, + address /* recipient */ + ) + external + returns (uint256) + { + _transferTokens(); + } + function _transferTokens() private { diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index cf9690c02..5a1fe5e16 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -52,17 +52,22 @@ contract ZeroExApiAdapter { bytes data; } + /* ============ State Variables ============ */ // ETH pseudo-token address used by 0x API. address private constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + // Minimum byte size of a single hop Uniswap V3 encoded path + uint256 private constant UNISWAP_V3_SINGLE_HOP_PATH_SIZE = 20 + 3 + 20; + // Address of the deployed ZeroEx contract. address public immutable zeroExAddress; // Returns the address to approve source tokens to for trading. This is the TokenTaker address address public immutable getSpender; + /* ============ constructor ============ */ constructor(address _zeroExAddress) public { @@ -152,7 +157,20 @@ contract ZeroExApiAdapter { inputToken = fillData.tokens[0]; outputToken = fillData.tokens[fillData.tokens.length - 1]; inputTokenAmount = fillData.sellAmount; - } else { + } else if (selector == 0x6af479b2) { + // sellTokenForTokenToUniswapV3() + { + bytes memory encodedPath; + (encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) = + abi.decode(_data[4:], (bytes, uint256, uint256, address)); + require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too short"); + } + // TODO(kimpers): Need to decode the path here + supportsRecipient = true; + inputToken = _sourceToken; + outputToken = _destinationToken; + } + else { revert("Unsupported 0xAPI function selector"); } } diff --git a/package.json b/package.json index b9036ce9a..37d3424d5 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "license": "MIT", "homepage": "https://github.com/SetProtocol", "devDependencies": { + "@0x/utils": "^6.4.3", "@ethersproject/bignumber": "^5.0.12", "@ethersproject/providers": "^5.0.17", "@nomiclabs/hardhat-ethers": "^2.0.1", diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index 389347dcb..f4e9e617b 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -5,6 +5,7 @@ import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES } from "@utils/constants"; import { ZeroExApiAdapter, ZeroExMock } from "@utils/contracts"; import DeployHelper from "@utils/deploys"; import { addSnapshotBeforeRestoreAfterEach, getAccounts, getWaffleExpect } from "@utils/test/index"; +import { hexUtils } from "@0x/utils"; const expect = getWaffleExpect(); @@ -29,12 +30,7 @@ describe("ZeroExApiAdapter", () => { deployer = new DeployHelper(owner.wallet); // Mock OneInch exchange that allows for only fixed exchange amounts - zeroExMock = await deployer.mocks.deployZeroExMock( - ADDRESS_ZERO, - ADDRESS_ZERO, - ZERO, - ZERO, - ); + zeroExMock = await deployer.mocks.deployZeroExMock(ADDRESS_ZERO, ADDRESS_ZERO, ZERO, ZERO); zeroExApiAdapter = await deployer.adapters.deployZeroExApiAdapter(zeroExMock.address); }); @@ -417,10 +413,10 @@ describe("ZeroExApiAdapter", () => { it("validates data", async () => { const data = zeroExMock.interface.encodeFunctionData("batchFill", [ { - inputToken: sourceToken, - outputToken: destToken, - sellAmount: sourceQuantity, - calls: [], + inputToken: sourceToken, + outputToken: destToken, + sellAmount: sourceQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -440,10 +436,10 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong input token", async () => { const data = zeroExMock.interface.encodeFunctionData("batchFill", [ { - inputToken: otherToken, - outputToken: destToken, - sellAmount: sourceQuantity, - calls: [], + inputToken: otherToken, + outputToken: destToken, + sellAmount: sourceQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -461,10 +457,10 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong output token", async () => { const data = zeroExMock.interface.encodeFunctionData("batchFill", [ { - inputToken: sourceToken, - outputToken: otherToken, - sellAmount: sourceQuantity, - calls: [], + inputToken: sourceToken, + outputToken: otherToken, + sellAmount: sourceQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -482,10 +478,10 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong input token quantity", async () => { const data = zeroExMock.interface.encodeFunctionData("batchFill", [ { - inputToken: sourceToken, - outputToken: destToken, - sellAmount: otherQuantity, - calls: [], + inputToken: sourceToken, + outputToken: destToken, + sellAmount: otherQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -503,10 +499,10 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong output token quantity", async () => { const data = zeroExMock.interface.encodeFunctionData("batchFill", [ { - inputToken: sourceToken, - outputToken: destToken, - sellAmount: sourceQuantity, - calls: [], + inputToken: sourceToken, + outputToken: destToken, + sellAmount: sourceQuantity, + calls: [], }, otherQuantity, ]); @@ -526,9 +522,9 @@ describe("ZeroExApiAdapter", () => { it("validates data", async () => { const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [ { - tokens: [sourceToken, destToken], - sellAmount: sourceQuantity, - calls: [], + tokens: [sourceToken, destToken], + sellAmount: sourceQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -548,9 +544,9 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong input token", async () => { const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [ { - tokens: [otherToken, destToken], - sellAmount: sourceQuantity, - calls: [], + tokens: [otherToken, destToken], + sellAmount: sourceQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -568,9 +564,9 @@ describe("ZeroExApiAdapter", () => { it("rejects went path too short", async () => { const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [ { - tokens: [sourceToken], - sellAmount: sourceQuantity, - calls: [], + tokens: [sourceToken], + sellAmount: sourceQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -588,9 +584,9 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong output token", async () => { const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [ { - tokens: [sourceToken, otherToken], - sellAmount: sourceQuantity, - calls: [], + tokens: [sourceToken, otherToken], + sellAmount: sourceQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -608,9 +604,9 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong input token quantity", async () => { const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [ { - tokens: [sourceToken, destToken], - sellAmount: otherQuantity, - calls: [], + tokens: [sourceToken, destToken], + sellAmount: otherQuantity, + calls: [], }, minDestinationQuantity, ]); @@ -628,9 +624,9 @@ describe("ZeroExApiAdapter", () => { it("rejects wrong output token quantity", async () => { const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [ { - tokens: [sourceToken, destToken], - sellAmount: sourceQuantity, - calls: [], + tokens: [sourceToken, destToken], + sellAmount: sourceQuantity, + calls: [], }, otherQuantity, ]); @@ -646,4 +642,40 @@ describe("ZeroExApiAdapter", () => { }); }); }); + describe.only("Uniswap V3", () => { + const POOL_FEE = 1234; + function encodePath(tokens_: string[]): string { + const elems: string[] = []; + tokens_.forEach((t, i) => { + if (i) { + elems.push(hexUtils.leftPad(POOL_FEE, 3)); + } + elems.push(hexUtils.leftPad(t, 20)); + }); + return hexUtils.concat(...elems); + } + + describe("sellTokenForTokenToUniswapV3", () => { + it("validates data", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([sourceToken, destToken]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + console.log(data); + const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + expect(target).to.eq(zeroExMock.address); + expect(value).to.deep.eq(ZERO); + expect(_data).to.deep.eq(data); + }); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 0502f6e19..c59f9b286 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,47 @@ # yarn lockfile v1 +"@0x/types@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@0x/types/-/types-3.3.3.tgz#5df4ec381bba9f62441474b0e54309ddb2fccd17" + integrity sha512-pImq1ukZl+YN64ZKQqNPTOK8noNw4rHMksEEPzFGM26x7Utovf8Py+VFqbZrn1TMw/9WWGeZg8lPxs+LUYxayw== + dependencies: + "@types/node" "12.12.54" + bignumber.js "~9.0.0" + ethereum-types "^3.5.0" + +"@0x/typescript-typings@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@0x/typescript-typings/-/typescript-typings-5.2.0.tgz#3eba353a27a83697f6f4f6d65ce14506687f52b7" + integrity sha512-8Gk0riQ37HTv3bNe/iWsb9mcJuRCMk/16PZTzA3IUauNQajcJgTD601pHbmBF57SJDpFhJIRg2Crcf6hePlzBA== + dependencies: + "@types/bn.js" "^4.11.0" + "@types/node" "12.12.54" + "@types/react" "*" + bignumber.js "~9.0.0" + ethereum-types "^3.5.0" + popper.js "1.14.3" + +"@0x/utils@^6.4.3": + version "6.4.3" + resolved "https://registry.yarnpkg.com/@0x/utils/-/utils-6.4.3.tgz#d7c710a8d8b8f2ee3a4e324dc71423078d93ff37" + integrity sha512-HxZ22/fGGL56BhKAtPWe9mAurhzwH775uL6uEpHWt0TuOgNAdDZHz5GhiCh6mTz+5Bf6jmk0/VroMcWUB8qFnw== + dependencies: + "@0x/types" "^3.3.3" + "@0x/typescript-typings" "^5.2.0" + "@types/mocha" "^5.2.7" + "@types/node" "12.12.54" + abortcontroller-polyfill "^1.1.9" + bignumber.js "~9.0.0" + chalk "^2.3.0" + detect-node "2.0.3" + ethereum-types "^3.5.0" + ethereumjs-util "^7.0.10" + ethers "~4.0.4" + isomorphic-fetch "2.2.1" + js-sha3 "^0.7.0" + lodash "^4.17.11" + "@babel/code-frame@^7.0.0": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -740,7 +781,7 @@ resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz#3c7750d0186b954c7f2d2f6acc8c3c7ba0c3412e" integrity sha512-wYxU3kp5zItbxKmeRYCEplS2MW7DzyBnxPGj+GJVHZEUZiK/nn5Ei1sUFgURDh+X051+zsGe28iud3oHjrYWQQ== -"@types/bn.js@*", "@types/bn.js@^4.11.3", "@types/bn.js@^4.11.4", "@types/bn.js@^4.11.5": +"@types/bn.js@*", "@types/bn.js@^4.11.0", "@types/bn.js@^4.11.3", "@types/bn.js@^4.11.4", "@types/bn.js@^4.11.5": version "4.11.6" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c" integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg== @@ -804,6 +845,11 @@ dependencies: "@types/node" "*" +"@types/mocha@^5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + "@types/mocha@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" @@ -822,6 +868,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== +"@types/node@12.12.54": + version "12.12.54" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1" + integrity sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w== + "@types/node@^10.12.18": version "10.17.50" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.50.tgz#7a20902af591282aa9176baefc37d4372131c32d" @@ -849,11 +900,25 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.6.tgz#f4b1efa784e8db479cdb8b14403e2144b1e9ff03" integrity sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/qs@^6.9.4": version "6.9.5" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== +"@types/react@*": + version "17.0.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.6.tgz#0ec564566302c562bf497d73219797a5e0297013" + integrity sha512-u/TtPoF/hrvb63LdukET6ncaplYsvCvmkceasx8oG84/ZCsoLxz9Z/raPBP4lTAiWW1Jb889Y9svHmv8R26dWw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@^0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -861,6 +926,11 @@ dependencies: "@types/node" "*" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/secp256k1@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.1.tgz#fb3aa61a1848ad97d7425ff9dcba784549fca5a4" @@ -923,6 +993,11 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abortcontroller-polyfill@^1.1.9: + version "1.7.3" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5" + integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q== + abstract-leveldown@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-3.0.0.tgz#5cb89f958a44f526779d740d1440e743e0c30a57" @@ -1861,7 +1936,7 @@ bech32@1.1.4: resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== -bignumber.js@^9.0.0: +bignumber.js@^9.0.0, bignumber.js@~9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== @@ -2702,6 +2777,11 @@ crypto-browserify@3.12.0: randombytes "^2.0.0" randomfill "^1.0.3" +csstype@^3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -2895,6 +2975,11 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +detect-node@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" + integrity sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc= + detect-port@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" @@ -3489,6 +3574,14 @@ ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +ethereum-types@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-3.5.0.tgz#27393f0d86f55bb1dcbff8a6af55a39c1f751c0d" + integrity sha512-vTGJl45DxOK21w3rzlqV8KrfcdIJC+4ZqxFkjNf1aw2GBMXZy2MxiibUqBth2M823d98WgOuFpVHobOfa7ejDw== + dependencies: + "@types/node" "12.12.54" + bignumber.js "~9.0.0" + ethereum-waffle@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-3.2.1.tgz#9d6d6b93484c5e1b77dfdeb646c050ed877e836e" @@ -3719,7 +3812,7 @@ ethereumjs-wallet@0.6.5: utf8 "^3.0.0" uuid "^3.3.2" -ethers@^4.0.32: +ethers@^4.0.32, ethers@~4.0.4: version "4.0.48" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.48.tgz#330c65b8133e112b0613156e57e92d9009d8fbbe" integrity sha512-sZD5K8H28dOrcidzx9f8KYh8083n5BexIO3+SbE4jK83L85FxtpXZBCQdXb8gkg+7sBqomcLhhkU7UHL+F7I2g== @@ -5401,6 +5494,14 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isomorphic-fetch@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -5454,6 +5555,11 @@ js-sha3@0.8.0, js-sha3@^0.8.0: resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== +js-sha3@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.7.0.tgz#0a5c57b36f79882573b2d84051f8bb85dd1bd63a" + integrity sha512-Wpks3yBDm0UcL5qlVhwW9Jr9n9i4FfeWBFOOXP5puDS/SiudJGhw7DPyBqn3487qD4F0lsC0q3zxink37f7zeA== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6556,12 +6662,7 @@ node-fetch@2.1.2: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= -node-fetch@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-fetch@~1.7.1: +node-fetch@^1.0.1, node-fetch@~1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== @@ -6569,6 +6670,11 @@ node-fetch@~1.7.1: encoding "^0.1.11" is-stream "^1.0.1" +node-fetch@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-gyp-build@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" @@ -7110,6 +7216,11 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +popper.js@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095" + integrity sha1-FDj5jQRqz3tNeM1QK/QYrGTU8JU= + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -9841,6 +9952,11 @@ whatwg-fetch@2.0.4: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== +whatwg-fetch@>=0.10.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" From 120a622b958423fd3d8a6b02133ae81da5e3cd19 Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Thu, 20 May 2021 16:12:46 +0200 Subject: [PATCH 02/10] feat: verify uniswap path for single hops WIP --- .../integration/exchange/ZeroExApiAdapter.sol | 34 ++++++++++++++----- .../exchange/zeroExApiAdapter.spec.ts | 19 +++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index 5a1fe5e16..5b6f7f04d 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -159,16 +159,12 @@ contract ZeroExApiAdapter { inputTokenAmount = fillData.sellAmount; } else if (selector == 0x6af479b2) { // sellTokenForTokenToUniswapV3() - { - bytes memory encodedPath; - (encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) = - abi.decode(_data[4:], (bytes, uint256, uint256, address)); - require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too short"); - } - // TODO(kimpers): Need to decode the path here + bytes memory encodedPath; + (encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) = + abi.decode(_data[4:], (bytes, uint256, uint256, address)); supportsRecipient = true; - inputToken = _sourceToken; - outputToken = _destinationToken; + + (inputToken, outputToken) = _decodePoolInfoFromPath(encodedPath); } else { revert("Unsupported 0xAPI function selector"); @@ -188,4 +184,24 @@ contract ZeroExApiAdapter { _data ); } + + // Return the first input token, output token, and fee of an encoded uniswap path. + function _decodePoolInfoFromPath(bytes memory encodedPath) + private + pure + returns ( + address inputToken, + address outputToken + ) + { + require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too shor too shortt"); + assembly { + let p := add(encodedPath, 32) + inputToken := shr(96, mload(p)) + p := add(p, 20) + // account for fee + p := add(p, 3) + outputToken := shr(96, mload(p)) + } + } } diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index f4e9e617b..510af5ec0 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -676,6 +676,25 @@ describe("ZeroExApiAdapter", () => { expect(value).to.deep.eq(ZERO); expect(_data).to.deep.eq(data); }); + it("multiple hops: validates data", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([sourceToken, otherToken, destToken]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + expect(target).to.eq(zeroExMock.address); + expect(value).to.deep.eq(ZERO); + expect(_data).to.deep.eq(data); + }); }); }); }); From da71703d608b9355aac6e1b6f4abf8ff4700791b Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Thu, 20 May 2021 17:42:22 +0200 Subject: [PATCH 03/10] feat: verify path for multiple hops --- .../integration/exchange/ZeroExApiAdapter.sol | 18 +++-- .../exchange/zeroExApiAdapter.spec.ts | 66 ++++++++----------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index 5b6f7f04d..918e99406 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -52,7 +52,6 @@ contract ZeroExApiAdapter { bytes data; } - /* ============ State Variables ============ */ // ETH pseudo-token address used by 0x API. @@ -60,6 +59,7 @@ contract ZeroExApiAdapter { // Minimum byte size of a single hop Uniswap V3 encoded path uint256 private constant UNISWAP_V3_SINGLE_HOP_PATH_SIZE = 20 + 3 + 20; + uint256 private constant UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE = 20 + 3; // Address of the deployed ZeroEx contract. address public immutable zeroExAddress; @@ -67,7 +67,6 @@ contract ZeroExApiAdapter { // Returns the address to approve source tokens to for trading. This is the TokenTaker address address public immutable getSpender; - /* ============ constructor ============ */ constructor(address _zeroExAddress) public { @@ -164,7 +163,17 @@ contract ZeroExApiAdapter { abi.decode(_data[4:], (bytes, uint256, uint256, address)); supportsRecipient = true; - (inputToken, outputToken) = _decodePoolInfoFromPath(encodedPath); + uint256 numHops = (encodedPath.length - 20)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; + require(numHops > 0, "At least 1 hop"); + + if (numHops == 1) { + (inputToken, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); + + } else { + uint256 lastPoolOffset = (numHops - 1) * UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; + (inputToken,) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); + (, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, lastPoolOffset); + } } else { revert("Unsupported 0xAPI function selector"); @@ -186,7 +195,7 @@ contract ZeroExApiAdapter { } // Return the first input token, output token, and fee of an encoded uniswap path. - function _decodePoolInfoFromPath(bytes memory encodedPath) + function _decodePoolInfoFromPathWithOffset(bytes memory encodedPath, uint256 offset) private pure returns ( @@ -197,6 +206,7 @@ contract ZeroExApiAdapter { require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too shor too shortt"); assembly { let p := add(encodedPath, 32) + p := add(p, offset) inputToken := shr(96, mload(p)) p := add(p, 20) // account for fee diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index 510af5ec0..eacd9ce6a 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -6,6 +6,7 @@ import { ZeroExApiAdapter, ZeroExMock } from "@utils/contracts"; import DeployHelper from "@utils/deploys"; import { addSnapshotBeforeRestoreAfterEach, getAccounts, getWaffleExpect } from "@utils/test/index"; import { hexUtils } from "@0x/utils"; +import { take } from "lodash"; const expect = getWaffleExpect(); @@ -14,6 +15,7 @@ describe("ZeroExApiAdapter", () => { const sourceToken = "0x6cf5f1d59fddae3a688210953a512b6aee6ea643"; const destToken = "0x5e5d0bea9d4a15db2d0837aff0435faba166190d"; const otherToken = "0xae9902bb655de1a67f334d8661b3ae6a96723d5b"; + const extraHopToken = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; const destination = "0x89b3515cad4f23c1deacea79fc12445cc21bd0e1"; const otherDestination = "0xdeb100c55cccfd6e39753f78c8b0c3bcbef86157"; const sourceQuantity = ONE; @@ -656,45 +658,31 @@ describe("ZeroExApiAdapter", () => { } describe("sellTokenForTokenToUniswapV3", () => { - it("validates data", async () => { - const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ - encodePath([sourceToken, destToken]), - sourceQuantity, - minDestinationQuantity, - destination, - ]); - console.log(data); - const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata( - sourceToken, - destToken, - destination, - sourceQuantity, - minDestinationQuantity, - data, - ); - expect(target).to.eq(zeroExMock.address); - expect(value).to.deep.eq(ZERO); - expect(_data).to.deep.eq(data); - }); - it("multiple hops: validates data", async () => { - const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ - encodePath([sourceToken, otherToken, destToken]), - sourceQuantity, - minDestinationQuantity, - destination, - ]); - const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata( - sourceToken, - destToken, - destination, - sourceQuantity, - minDestinationQuantity, - data, - ); - expect(target).to.eq(zeroExMock.address); - expect(value).to.deep.eq(ZERO); - expect(_data).to.deep.eq(data); - }); + const additionalHops = [otherToken, extraHopToken]; + for (let i = 0; i <= additionalHops.length; i++) { + const hops = take(additionalHops, i); + it(`validates data for ${i + 1} hops`, async () => { + const path = [sourceToken, ...hops, destToken]; + + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath(path), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + expect(target).to.eq(zeroExMock.address); + expect(value).to.deep.eq(ZERO); + expect(_data).to.deep.eq(data); + }); + } }); }); }); From b09993009aa835e7001231843a15898e480ba578 Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Wed, 26 May 2021 15:02:55 +0200 Subject: [PATCH 04/10] fix: add proper tests, require statements for sellTokenForToken --- .../integration/exchange/ZeroExApiAdapter.sol | 3 +- .../exchange/zeroExApiAdapter.spec.ts | 108 ++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index 918e99406..658c6254f 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -163,8 +163,9 @@ contract ZeroExApiAdapter { abi.decode(_data[4:], (bytes, uint256, uint256, address)); supportsRecipient = true; + require(encodedPath.length > 20, "UniswapV3 token path too short"); uint256 numHops = (encodedPath.length - 20)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; - require(numHops > 0, "At least 1 hop"); + require(numHops > 0, "UniswapV3 token path too short"); if (numHops == 1) { (inputToken, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index eacd9ce6a..e8f291b27 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -683,6 +683,114 @@ describe("ZeroExApiAdapter", () => { expect(_data).to.deep.eq(data); }); } + + it("rejects wrong input token", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([otherToken, destToken]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched input token"); + }); + + it("rejects wrong output token", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([sourceToken, otherToken]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched output token"); + }); + + it("rejects wrong input token quantity", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([sourceToken, destToken]), + otherQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched input token quantity"); + }); + + it("rejects wrong output token quantity", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([sourceToken, destToken]), + sourceQuantity, + otherQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched output token quantity"); + }); + + it("rejects invalid uniswap path", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([sourceToken]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("UniswapV3 token path too short"); + }); + + it("rejects wrong destination", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [ + encodePath([sourceToken, destToken]), + sourceQuantity, + minDestinationQuantity, + ADDRESS_ZERO, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + destToken, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched recipient"); + }); }); }); }); From e3a42ebe771d6c780f9eac02c020e69db4f1fd03 Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Wed, 26 May 2021 15:10:53 +0200 Subject: [PATCH 05/10] refactor: move decoding tokens from encoded path into reusable function --- .../integration/exchange/ZeroExApiAdapter.sol | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index 658c6254f..93105faf8 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -162,19 +162,7 @@ contract ZeroExApiAdapter { (encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) = abi.decode(_data[4:], (bytes, uint256, uint256, address)); supportsRecipient = true; - - require(encodedPath.length > 20, "UniswapV3 token path too short"); - uint256 numHops = (encodedPath.length - 20)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; - require(numHops > 0, "UniswapV3 token path too short"); - - if (numHops == 1) { - (inputToken, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); - - } else { - uint256 lastPoolOffset = (numHops - 1) * UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; - (inputToken,) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); - (, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, lastPoolOffset); - } + (inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); } else { revert("Unsupported 0xAPI function selector"); @@ -195,7 +183,30 @@ contract ZeroExApiAdapter { ); } - // Return the first input token, output token, and fee of an encoded uniswap path. + // Decode input and output tokens from arbitrary length encoded Uniswap V3 path + function _decodeTokensFromUniswapV3EncodedPath(bytes memory encodedPath) + private + pure + returns ( + address inputToken, + address outputToken + ) + { + require(encodedPath.length > 20, "UniswapV3 token path too short"); + uint256 numHops = (encodedPath.length - 20)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; + require(numHops > 0, "UniswapV3 token path too short"); + + if (numHops == 1) { + (inputToken, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); + + } else { + uint256 lastPoolOffset = (numHops - 1) * UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; + (inputToken,) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); + (, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, lastPoolOffset); + } + } + + // Return the input and output token at a specified offset in the encoded Uniswap V3 path function _decodePoolInfoFromPathWithOffset(bytes memory encodedPath, uint256 offset) private pure From 583980b4df21dfe781436fda7b571f4c9a569fdb Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Wed, 26 May 2021 16:27:57 +0200 Subject: [PATCH 06/10] feat: implement sellTokenForEthToUniswapV3 and sellEthForTokenToUniswapV3 --- .../integration/exchange/ZeroExApiAdapter.sol | 24 +- .../exchange/zeroExApiAdapter.spec.ts | 253 +++++++++++++++++- 2 files changed, 271 insertions(+), 6 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index 93105faf8..ed804b3e4 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -57,8 +57,9 @@ contract ZeroExApiAdapter { // ETH pseudo-token address used by 0x API. address private constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - // Minimum byte size of a single hop Uniswap V3 encoded path + // Minimum byte size of a single hop Uniswap V3 encoded path (token address + fee + token adress) uint256 private constant UNISWAP_V3_SINGLE_HOP_PATH_SIZE = 20 + 3 + 20; + // Byte size of one hop in the Uniswap V3 encoded path (token address + fee) uint256 private constant UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE = 20 + 3; // Address of the deployed ZeroEx contract. @@ -163,8 +164,23 @@ contract ZeroExApiAdapter { abi.decode(_data[4:], (bytes, uint256, uint256, address)); supportsRecipient = true; (inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); - } - else { + } else if (selector == 0x803ba26d) { + // sellTokenForEthToUniswapV3() + bytes memory encodedPath; + (encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) = + abi.decode(_data[4:], (bytes, uint256, uint256, address)); + supportsRecipient = true; + (inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); + } else if (selector == 0x3598d8ab) { + // sellEthForTokenToUniswapV3() + // TODO(kimpers): is this correct? + inputTokenAmount = 0; + bytes memory encodedPath; + (encodedPath, minOutputTokenAmount, recipient) = + abi.decode(_data[4:], (bytes, uint256, address)); + supportsRecipient = true; + (inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); + } else { revert("Unsupported 0xAPI function selector"); } } @@ -215,7 +231,7 @@ contract ZeroExApiAdapter { address outputToken ) { - require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too shor too shortt"); + require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "UniswapV3 token path too short"); assembly { let p := add(encodedPath, 32) p := add(p, offset) diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index e8f291b27..a25abb6cd 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -1,7 +1,7 @@ import "module-alias/register"; import { Account } from "@utils/test/types"; -import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES } from "@utils/constants"; +import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES, ETH_ADDRESS } from "@utils/constants"; import { ZeroExApiAdapter, ZeroExMock } from "@utils/contracts"; import DeployHelper from "@utils/deploys"; import { addSnapshotBeforeRestoreAfterEach, getAccounts, getWaffleExpect } from "@utils/test/index"; @@ -644,7 +644,7 @@ describe("ZeroExApiAdapter", () => { }); }); }); - describe.only("Uniswap V3", () => { + describe("Uniswap V3", () => { const POOL_FEE = 1234; function encodePath(tokens_: string[]): string { const elems: string[] = []; @@ -792,5 +792,254 @@ describe("ZeroExApiAdapter", () => { await expect(tx).to.be.revertedWith("Mismatched recipient"); }); }); + + describe("sellTokenForEthToUniswapV3", () => { + const additionalHops = [otherToken, extraHopToken]; + for (let i = 0; i <= additionalHops.length; i++) { + const hops = take(additionalHops, i); + it(`validates data for ${i + 1} hops`, async () => { + const path = [sourceToken, ...hops, ETH_ADDRESS]; + + const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [ + encodePath(path), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata( + sourceToken, + ETH_ADDRESS, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + expect(target).to.eq(zeroExMock.address); + expect(value).to.deep.eq(ZERO); + expect(_data).to.deep.eq(data); + }); + } + + it("rejects wrong input token", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [ + encodePath([otherToken, ETH_ADDRESS]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + ETH_ADDRESS, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched input token"); + }); + + it("rejects wrong output token", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [ + encodePath([sourceToken, otherToken]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + ETH_ADDRESS, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched output token"); + }); + + it("rejects wrong input token quantity", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [ + encodePath([sourceToken, ETH_ADDRESS]), + otherQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + ETH_ADDRESS, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched input token quantity"); + }); + + it("rejects wrong output token quantity", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [ + encodePath([sourceToken, ETH_ADDRESS]), + sourceQuantity, + otherQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + ETH_ADDRESS, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched output token quantity"); + }); + + it("rejects invalid uniswap path", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [ + encodePath([sourceToken]), + sourceQuantity, + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + ETH_ADDRESS, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("UniswapV3 token path too short"); + }); + + it("rejects wrong destination", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [ + encodePath([sourceToken, ETH_ADDRESS]), + sourceQuantity, + minDestinationQuantity, + ADDRESS_ZERO, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + sourceToken, + ETH_ADDRESS, + destination, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched recipient"); + }); + }); + + describe("sellEthForTokenToUniswapV3", () => { + const additionalHops = [otherToken, extraHopToken]; + for (let i = 0; i <= additionalHops.length; i++) { + const hops = take(additionalHops, i); + it(`validates data for ${i + 1} hops`, async () => { + const path = [ETH_ADDRESS, ...hops, destToken]; + + const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ + encodePath(path), + minDestinationQuantity, + destination, + ]); + const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata( + ETH_ADDRESS, + destToken, + destination, + ZERO, + minDestinationQuantity, + data, + ); + expect(target).to.eq(zeroExMock.address); + // TODO(kimpers): is value 0 correct here? + expect(value).to.deep.eq(ZERO); + expect(_data).to.deep.eq(data); + }); + } + + it("rejects wrong input token", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ + encodePath([otherToken, destToken]), + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + ETH_ADDRESS, + destToken, + destination, + ZERO, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched input token"); + }); + + it("rejects wrong output token", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ + encodePath([ETH_ADDRESS, otherToken]), + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + ETH_ADDRESS, + destToken, + destination, + ZERO, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched output token"); + }); + + it("rejects wrong output token quantity", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ + encodePath([ETH_ADDRESS, destToken]), + otherQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + ETH_ADDRESS, + destToken, + destination, + ZERO, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched output token quantity"); + }); + + it("rejects invalid uniswap path", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ + encodePath([ETH_ADDRESS]), + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + ETH_ADDRESS, + destToken, + destination, + ZERO, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("UniswapV3 token path too short"); + }); + + it("rejects wrong destination", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ + encodePath([ETH_ADDRESS, destToken]), + minDestinationQuantity, + ADDRESS_ZERO, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + ETH_ADDRESS, + destToken, + destination, + ZERO, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("Mismatched recipient"); + }); + }); }); }); From e5122cf29a9b432a05d938b341c5f34e171de25a Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Thu, 27 May 2021 10:58:00 +0200 Subject: [PATCH 07/10] fix: address review feedback --- .../protocol/integration/exchange/ZeroExApiAdapter.sol | 9 ++++----- .../integration/exchange/zeroExApiAdapter.spec.ts | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index ed804b3e4..f9f345e7b 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -173,13 +173,13 @@ contract ZeroExApiAdapter { (inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); } else if (selector == 0x3598d8ab) { // sellEthForTokenToUniswapV3() - // TODO(kimpers): is this correct? - inputTokenAmount = 0; + inputTokenAmount = _sourceQuantity; bytes memory encodedPath; (encodedPath, minOutputTokenAmount, recipient) = abi.decode(_data[4:], (bytes, uint256, address)); supportsRecipient = true; - (inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); + inputToken = ETH_ADDRESS; + (, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); } else { revert("Unsupported 0xAPI function selector"); } @@ -208,9 +208,8 @@ contract ZeroExApiAdapter { address outputToken ) { - require(encodedPath.length > 20, "UniswapV3 token path too short"); + require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "UniswapV3 token path too short"); uint256 numHops = (encodedPath.length - 20)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; - require(numHops > 0, "UniswapV3 token path too short"); if (numHops == 1) { (inputToken, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index a25abb6cd..0a551d5f1 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -950,8 +950,7 @@ describe("ZeroExApiAdapter", () => { data, ); expect(target).to.eq(zeroExMock.address); - // TODO(kimpers): is value 0 correct here? - expect(value).to.deep.eq(ZERO); + expect(value).to.deep.eq(sourceQuantity); expect(_data).to.deep.eq(data); }); } From a594881f5da412499f77744c4fd11ee0e91d6b39 Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Thu, 27 May 2021 11:30:40 +0200 Subject: [PATCH 08/10] fix: sellEthForTokenToUniswapV3 impl and address review comments --- .../integration/exchange/ZeroExApiAdapter.sol | 4 ++- .../exchange/zeroExApiAdapter.spec.ts | 31 ++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index f9f345e7b..2c9fb033f 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -178,8 +178,10 @@ contract ZeroExApiAdapter { (encodedPath, minOutputTokenAmount, recipient) = abi.decode(_data[4:], (bytes, uint256, address)); supportsRecipient = true; + address _decodedInputToken; + (_decodedInputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); + require(_decodedInputToken == ETH_ADDRESS, "sellEthForToken requires ETH input token"); inputToken = ETH_ADDRESS; - (, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); } else { revert("Unsupported 0xAPI function selector"); } diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index 0a551d5f1..c43d8d18d 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -945,7 +945,7 @@ describe("ZeroExApiAdapter", () => { ETH_ADDRESS, destToken, destination, - ZERO, + sourceQuantity, minDestinationQuantity, data, ); @@ -955,7 +955,7 @@ describe("ZeroExApiAdapter", () => { }); } - it("rejects wrong input token", async () => { + it("rejects non ETH input token", async () => { const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ encodePath([otherToken, destToken]), minDestinationQuantity, @@ -965,7 +965,24 @@ describe("ZeroExApiAdapter", () => { ETH_ADDRESS, destToken, destination, - ZERO, + sourceQuantity, + minDestinationQuantity, + data, + ); + await expect(tx).to.be.revertedWith("sellEthForToken requires ETH input token"); + }); + + it("rejects wrong input token", async () => { + const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ + encodePath([ETH_ADDRESS, destToken]), + minDestinationQuantity, + destination, + ]); + const tx = zeroExApiAdapter.getTradeCalldata( + otherToken, + destToken, + destination, + sourceQuantity, minDestinationQuantity, data, ); @@ -982,7 +999,7 @@ describe("ZeroExApiAdapter", () => { ETH_ADDRESS, destToken, destination, - ZERO, + sourceQuantity, minDestinationQuantity, data, ); @@ -999,7 +1016,7 @@ describe("ZeroExApiAdapter", () => { ETH_ADDRESS, destToken, destination, - ZERO, + sourceQuantity, minDestinationQuantity, data, ); @@ -1016,7 +1033,7 @@ describe("ZeroExApiAdapter", () => { ETH_ADDRESS, destToken, destination, - ZERO, + sourceQuantity, minDestinationQuantity, data, ); @@ -1033,7 +1050,7 @@ describe("ZeroExApiAdapter", () => { ETH_ADDRESS, destToken, destination, - ZERO, + sourceQuantity, minDestinationQuantity, data, ); From 0be67c86e13803d0c895187ca7aa2828c3009796 Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Thu, 27 May 2021 14:19:48 +0200 Subject: [PATCH 09/10] refactor: clean up Uniswap V3 path decoding logic --- .../integration/exchange/ZeroExApiAdapter.sol | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index 2c9fb033f..0f2a4bc66 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -57,10 +57,13 @@ contract ZeroExApiAdapter { // ETH pseudo-token address used by 0x API. address private constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + // Byte size of Uniswap V3 encoded path addresses and pool fees + uint256 private constant UNISWAP_V3_PATH_ADDRESS_SIZE = 20; + uint256 private constant UNISWAP_V3_PATH_FEE_SIZE = 3; // Minimum byte size of a single hop Uniswap V3 encoded path (token address + fee + token adress) - uint256 private constant UNISWAP_V3_SINGLE_HOP_PATH_SIZE = 20 + 3 + 20; + uint256 private constant UNISWAP_V3_SINGLE_HOP_PATH_SIZE = UNISWAP_V3_PATH_ADDRESS_SIZE + UNISWAP_V3_PATH_FEE_SIZE + UNISWAP_V3_PATH_ADDRESS_SIZE; // Byte size of one hop in the Uniswap V3 encoded path (token address + fee) - uint256 private constant UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE = 20 + 3; + uint256 private constant UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE = UNISWAP_V3_PATH_ADDRESS_SIZE + UNISWAP_V3_PATH_FEE_SIZE; // Address of the deployed ZeroEx contract. address public immutable zeroExAddress; @@ -201,7 +204,7 @@ contract ZeroExApiAdapter { ); } - // Decode input and output tokens from arbitrary length encoded Uniswap V3 path + // Decode input and output tokens from an arbitrary length encoded Uniswap V3 path function _decodeTokensFromUniswapV3EncodedPath(bytes memory encodedPath) private pure @@ -211,35 +214,15 @@ contract ZeroExApiAdapter { ) { require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "UniswapV3 token path too short"); - uint256 numHops = (encodedPath.length - 20)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; - if (numHops == 1) { - (inputToken, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); - - } else { - uint256 lastPoolOffset = (numHops - 1) * UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; - (inputToken,) = _decodePoolInfoFromPathWithOffset(encodedPath, 0); - (, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, lastPoolOffset); - } - } - - // Return the input and output token at a specified offset in the encoded Uniswap V3 path - function _decodePoolInfoFromPathWithOffset(bytes memory encodedPath, uint256 offset) - private - pure - returns ( - address inputToken, - address outputToken - ) - { - require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "UniswapV3 token path too short"); + // UniswapV3 paths are packed encoded as (address(token0), uint24(fee), address(token1), [...]) + // We want the first and last token. + uint256 numHops = (encodedPath.length - UNISWAP_V3_PATH_ADDRESS_SIZE)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; + uint256 lastTokenOffset = numHops * UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE; assembly { let p := add(encodedPath, 32) - p := add(p, offset) inputToken := shr(96, mload(p)) - p := add(p, 20) - // account for fee - p := add(p, 3) + p := add(p, lastTokenOffset) outputToken := shr(96, mload(p)) } } From 161f14af7f2d010ff80c5e5f791502f7174bcfad Mon Sep 17 00:00:00 2001 From: Kim Persson Date: Thu, 27 May 2021 17:16:21 +0200 Subject: [PATCH 10/10] fix: remove incorrect require check for sellEthForTokenToUniswapV3 --- .../integration/exchange/ZeroExApiAdapter.sol | 4 +--- .../exchange/zeroExApiAdapter.spec.ts | 17 ----------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol index 0f2a4bc66..87dffac42 100644 --- a/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol +++ b/contracts/protocol/integration/exchange/ZeroExApiAdapter.sol @@ -181,9 +181,7 @@ contract ZeroExApiAdapter { (encodedPath, minOutputTokenAmount, recipient) = abi.decode(_data[4:], (bytes, uint256, address)); supportsRecipient = true; - address _decodedInputToken; - (_decodedInputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); - require(_decodedInputToken == ETH_ADDRESS, "sellEthForToken requires ETH input token"); + (, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath); inputToken = ETH_ADDRESS; } else { revert("Unsupported 0xAPI function selector"); diff --git a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts index c43d8d18d..01ef46648 100644 --- a/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts +++ b/test/protocol/integration/exchange/zeroExApiAdapter.spec.ts @@ -955,23 +955,6 @@ describe("ZeroExApiAdapter", () => { }); } - it("rejects non ETH input token", async () => { - const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ - encodePath([otherToken, destToken]), - minDestinationQuantity, - destination, - ]); - const tx = zeroExApiAdapter.getTradeCalldata( - ETH_ADDRESS, - destToken, - destination, - sourceQuantity, - minDestinationQuantity, - data, - ); - await expect(tx).to.be.revertedWith("sellEthForToken requires ETH input token"); - }); - it("rejects wrong input token", async () => { const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [ encodePath([ETH_ADDRESS, destToken]),