diff --git a/README.md b/README.md index af26c94..02fc50e 100644 --- a/README.md +++ b/README.md @@ -109,15 +109,21 @@ This section presents detailed gas usage data for various swapping scenarios acr │ DAI => WETH => USDC => USDT │ 244,547 │ 253,788 │ 257,736 │ 255,069 │ └─────────────────────────────┴─────────┴─────────┴─────────┴──────────┘ ``` -# Mixed pools +### Mixed pools ``` -┌────────────────────────────────────────────────────────┐ -│ UniswapV2 => UniswapV3 pools │ -├────────────────────────────────────┬─────────┬─────────┤ -│ path │ 1inch │ uniswap │ -├────────────────────────────────────┼─────────┼─────────┤ -│ ETH =[uniV2]=> DAI =[uniV3]=> USDC │ 160,935 │ 177,244 │ -└────────────────────────────────────┴─────────┴─────────┘ +┌─────────────────────────────────────────────────────────┐ +│ Mixed pools │ +├─────────────────────────────────────┬─────────┬─────────┤ +│ path │ 1inch │ uniswap │ +├─────────────────────────────────────┼─────────┼─────────┤ +│ ETH =(uniV2)=> DAI =(uniV3)=> USDC │ 158,538 │ 177,220 │ +├─────────────────────────────────────┼─────────┼─────────┤ +│ ETH =(uniV3)=> DAI =(uniV2)=> USDC │ 161,861 │ 186,960 │ +├─────────────────────────────────────┼─────────┼─────────┤ +│ DAI =(uniV2)=> WETH =(uniV3)=> USDC │ 165,116 │ 185,915 │ +├─────────────────────────────────────┼─────────┼─────────┤ +│ DAI =(uniV3)=> WETH =(uniV2)=> USDC │ 162,008 │ 189,087 │ +└─────────────────────────────────────┴─────────┴─────────┘ ``` ## Contribution diff --git a/hardhat.config.js b/hardhat.config.js index 353f204..379ab40 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -44,6 +44,7 @@ module.exports = { '@1inch/limit-order-settlement/contracts/Settlement.sol', '@1inch/solidity-utils/contracts/interfaces/IWETH.sol', '@openzeppelin/contracts/token/ERC20/IERC20.sol', + '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol', '@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol', '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol', '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol', diff --git a/test/LimitOrders.js b/test/LimitOrders.js index 757a439..1a2df09 100644 --- a/test/LimitOrders.js +++ b/test/LimitOrders.js @@ -2,7 +2,7 @@ const hre = require('hardhat'); const { ethers, getChainId } = hre; const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { NonceManager } = require('ethers'); -const { ether, constants } = require('@1inch/solidity-utils'); +const { ether, constants, permit2Contract } = require('@1inch/solidity-utils'); const { fillWithMakingAmount, buildMakerTraits } = require('@1inch/limit-order-protocol-contract/test/helpers/orderUtils'); const { InchOrder, MatchaOrder, UniswapOrder, ParaswapOrder } = require('./helpers/orders'); const { expect } = require('chai'); @@ -11,7 +11,6 @@ const { createGasUsedTable } = require('./helpers/table'); const PARASWAP_TOKEN_TRANSFER_PROXY = '0x216B4B4Ba9F3e719726886d34a177484278Bfcae'; const PARASWAP_LIMIT_ORDERS = '0xe92b586627ccA7a83dC919cc7127196d70f55a06'; -const PERMIT2CONTRACT = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; describe('LimitOrders', async function () { const gasUsedTable = createGasUsedTable('Limit Orders', 'case'); @@ -26,6 +25,7 @@ describe('LimitOrders', async function () { const inch = await ethers.getContractAt('LimitOrderProtocol', '0x111111125421ca6dc452d289314280a0f8842a65'); const uniswap = await ethers.getContractAt('IReactor', '0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4'); const matcha = await ethers.getContractAt('IMatcha', '0xDef1C0ded9bec7F1a1670819833240f027b25EfF'); + const permit2 = await permit2Contract(); const tokens = { ETH: { @@ -54,11 +54,11 @@ describe('LimitOrders', async function () { await token.connect(wallet).approve(uniswap, ether('1')); await token.connect(wallet).approve(PARASWAP_TOKEN_TRANSFER_PROXY, ether('1')); await token.connect(wallet).approve(PARASWAP_LIMIT_ORDERS, ether('1')); - await token.connect(wallet).approve(PERMIT2CONTRACT, ether('1')); + await token.connect(wallet).approve(permit2, ether('1')); } } - return { maker, taker, tokens, inch, matcha, uniswap }; + return { maker, taker, tokens, inch, matcha, uniswap, permit2 }; } describe('ETH => DAI', async function () { @@ -112,6 +112,7 @@ describe('LimitOrders', async function () { maker, taker, uniswap, + permit2, settings: { gasUsedTableRow, makerToken, takerToken, makingAmount, takingAmount }, } = await loadFixture(initContractsWithCaseSettings); @@ -126,7 +127,7 @@ describe('LimitOrders', async function () { outputTokenAddress: await takerToken.getAddress(), inputAmount: makingAmount, outputAmount: takingAmount, - permit2contractAddress: PERMIT2CONTRACT, + permit2contractAddress: await permit2.getAddress(), }); const signedOrder = await uniswapOrder.sign(maker); const tx = await uniswap.connect(taker).execute(signedOrder, { value: uniswapOrder.order.info.outputs[0].startAmount }); @@ -218,6 +219,7 @@ describe('LimitOrders', async function () { maker, taker, uniswap, + permit2, settings: { gasUsedTableRow, makerToken, takerToken, makingAmount, takingAmount }, } = await loadFixture(initContractsWithCaseSettings); @@ -232,7 +234,7 @@ describe('LimitOrders', async function () { outputTokenAddress: await takerToken.getAddress(), inputAmount: makingAmount, outputAmount: takingAmount, - permit2contractAddress: PERMIT2CONTRACT, + permit2contractAddress: await permit2.getAddress(), }); const signedOrder = await uniswapOrder.sign(maker); const tx = await uniswap.connect(taker).execute(signedOrder); diff --git a/test/RouterMixedPools.js b/test/RouterMixedPools.js new file mode 100644 index 0000000..3b0a3f2 --- /dev/null +++ b/test/RouterMixedPools.js @@ -0,0 +1,241 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ether, permit2Contract } = require('@1inch/solidity-utils'); +const { ProtocolKey, uniswapMixedPoolsData } = require('./helpers/utils'); +const { initRouterContracts, adjustV2PoolTimestamps } = require('./helpers/fixtures'); +const { createGasUsedTable } = require('./helpers/table'); +const { UniswapV2Pools, UniswapV3Pools } = require('./helpers/pools'); + +describe('Mixed pools', async function () { + const gasUsedTable = createGasUsedTable('Mixed pools', 'path'); + + after(async function () { + console.log(gasUsedTable.toString()); + }); + + async function initContracts() { + const fixtureData = await initRouterContracts(); + + await adjustV2PoolTimestamps(ethers, UniswapV2Pools); + + return fixtureData; + } + + async function initContractsWithApproveInPermit2() { + // This fixture is used to approve tokens in the permit2 contract and doesn't use permit functionality + const fixtureData = await initContracts(); + + // TODO: add this abi method to the interface in solidity-utils + const abi = ['function approve(address token, address spender, uint160 amount, uint48 expiration) external']; + const permit2 = await ethers.getContractAt(abi, (await permit2Contract()).target); + await fixtureData.tokens.DAI.approve(permit2, ether('1')); + await permit2.approve(fixtureData.tokens.DAI, fixtureData.uniswapUniversalRouter, ether('1'), Date.now()); + await permit2.approve(fixtureData.tokens.DAI, fixtureData.inch, ether('1'), Date.now()); + + return fixtureData; + } + + describe('ETH =(uniV2)=> DAI =(uniV3)=> USDC', async function () { + async function initContractsWithCaseSettings() { + return { + ...(await initContracts()), + settings: { + gasUsedTableRow: gasUsedTable.addRow(['ETH =(uniV2)=> DAI =(uniV3)=> USDC']), + amount: ether('1'), + }, + }; + } + + it('1inch', async function () { + const { + addr1, + inch, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await inch.ethUnoswapTo2( + addr1.address, + '1', + BigInt(UniswapV2Pools.WETH_DAI), + BigInt(UniswapV3Pools.USDC_DAI.address) | (1n << 253n) | (1n << 247n), + { + value: amount, + }, + ); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.INCH, (await tx.wait()).gasUsed); + }); + + it('uniswap', async function () { + const { + addr1, + uniswapUniversalRouter, + tokens, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await addr1.sendTransaction({ + to: uniswapUniversalRouter.getAddress(), + ...(await uniswapMixedPoolsData( + [UniswapV2Pools.WETH_DAI, UniswapV3Pools.USDC_DAI], + [tokens.WETH.target, tokens.DAI.target, tokens.USDC.target], + 1, + amount, + )), + }); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.UNISWAP, (await tx.wait()).gasUsed); + }); + }); + + describe('ETH =(uniV3)=> DAI =(uniV2)=> USDC', async function () { + async function initContractsWithCaseSettings() { + return { + ...(await initContracts()), + settings: { + gasUsedTableRow: gasUsedTable.addRow(['ETH =(uniV3)=> DAI =(uniV2)=> USDC']), + amount: ether('1'), + }, + }; + } + + it('1inch', async function () { + const { + addr1, + inch, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await inch.ethUnoswapTo2( + addr1.address, + '1', + BigInt(UniswapV3Pools.WETH_DAI.address) | (1n << 253n), + BigInt(UniswapV2Pools.USDC_DAI) | (1n << 247n), + { + value: amount, + }, + ); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.INCH, (await tx.wait()).gasUsed); + }); + + it('uniswap', async function () { + const { + addr1, + uniswapUniversalRouter, + tokens, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await addr1.sendTransaction({ + to: uniswapUniversalRouter.getAddress(), + ...(await uniswapMixedPoolsData( + [UniswapV3Pools.WETH_DAI, UniswapV2Pools.USDC_DAI], + [tokens.WETH.target, tokens.DAI.target, tokens.USDC.target], + 1, + amount, + )), + }); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.UNISWAP, (await tx.wait()).gasUsed); + }); + }); + + describe('DAI =(uniV2)=> WETH =(uniV3)=> USDC', async function () { + async function initContractsWithCaseSettings() { + return { + ...(await initContractsWithApproveInPermit2()), + settings: { + gasUsedTableRow: gasUsedTable.addRow(['DAI =(uniV2)=> WETH =(uniV3)=> USDC']), + amount: ether('1'), + }, + }; + } + + it('1inch', async function () { + const { + addr1, + inch, + tokens, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await inch.unoswapTo2( + addr1.address, + tokens.DAI.target, + amount, + '1', + BigInt(UniswapV2Pools.WETH_DAI) | (1n << 247n), + BigInt(UniswapV3Pools.WETH_USDC.address) | (1n << 253n), + ); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.INCH, (await tx.wait()).gasUsed); + }); + + it('uniswap', async function () { + const { + addr1, + uniswapUniversalRouter, + tokens, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await addr1.sendTransaction({ + to: uniswapUniversalRouter.getAddress(), + ...(await uniswapMixedPoolsData( + [UniswapV2Pools.WETH_DAI, UniswapV3Pools.WETH_USDC], + [tokens.DAI.target, tokens.WETH.target, tokens.USDC.target], + 0, + amount, + )), + }); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.UNISWAP, (await tx.wait()).gasUsed); + }); + }); + + describe('DAI =(uniV3)=> WETH =(uniV2)=> USDC', async function () { + async function initContractsWithCaseSettings() { + return { + ...(await initContractsWithApproveInPermit2()), + settings: { + gasUsedTableRow: gasUsedTable.addRow(['DAI =(uniV3)=> WETH =(uniV2)=> USDC']), + amount: ether('1'), + }, + }; + } + + it('1inch', async function () { + const { + addr1, + inch, + tokens, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await inch.unoswapTo2( + addr1.address, + tokens.DAI.target, + amount, + '1', + BigInt(UniswapV3Pools.WETH_DAI.address) | (1n << 253n) | (1n << 247n), + UniswapV2Pools.WETH_USDC, + ); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.INCH, (await tx.wait()).gasUsed); + }); + + it('uniswap', async function () { + const { + addr1, + uniswapUniversalRouter, + tokens, + settings: { gasUsedTableRow, amount }, + } = await loadFixture(initContractsWithCaseSettings); + + const tx = await addr1.sendTransaction({ + to: uniswapUniversalRouter.getAddress(), + ...(await uniswapMixedPoolsData( + [UniswapV3Pools.WETH_DAI, UniswapV2Pools.WETH_USDC], + [tokens.DAI.target, tokens.WETH.target, tokens.USDC.target], + 0, + amount, + )), + }); + gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.UNISWAP, (await tx.wait()).gasUsed); + }); + }); +}); diff --git a/test/RouterUniV2V3.js b/test/RouterUniV2V3.js deleted file mode 100644 index c827071..0000000 --- a/test/RouterUniV2V3.js +++ /dev/null @@ -1,124 +0,0 @@ -const { ethers } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { ether } = require('@1inch/solidity-utils'); -const { ProtocolKey } = require('./helpers/utils'); -const { initRouterContracts, adjustV2PoolTimestamps } = require('./helpers/fixtures'); -const { createGasUsedTable } = require('./helpers/table'); -const { MixedRouteTrade, MixedRouteSDK, Protocol } = require('@uniswap/router-sdk'); -const { SwapRouter, UniswapTrade } = require('@uniswap/universal-router-sdk'); -const { Pool } = require('@uniswap/v3-sdk'); -const { Pair } = require('@uniswap/v2-sdk'); -const { CurrencyAmount, Token, TradeType, Ether, Percent } = require('@uniswap/sdk-core'); -const { UniswapV2Pools, UniswapV3Pools } = require('./helpers/pools'); - -describe('Router [UniV2 => UniV3]', async function () { - const gasUsedTable = createGasUsedTable('UniswapV2 => UniswapV3 pools', 'path'); - - after(async function () { - console.log(gasUsedTable.toString()); - }); - - async function initContracts() { - const fixtureData = await initRouterContracts(); - - await adjustV2PoolTimestamps(ethers, UniswapV2Pools); - - return fixtureData; - } - - describe('ETH =[uniV2]=> DAI =[uniV3]=> USDC', async function () { - async function initContractsWithCaseSettings() { - return { - ...(await initContracts()), - settings: { - gasUsedTableRow: gasUsedTable.addRow(['ETH =[uniV2]=> DAI =[uniV3]=> USDC']), - amount: ether('1'), - }, - }; - } - - it('1inch', async function () { - const { - addr1, - inch, - settings: { gasUsedTableRow, amount }, - } = await loadFixture(initContractsWithCaseSettings); - - const tx = await inch.ethUnoswapTo2( - addr1.address, - '1', - BigInt(UniswapV2Pools.WETH_DAI), - BigInt(UniswapV3Pools.USDC_DAI.address) | (1n << 253n) | (1n << 247n), - { - value: amount, - }, - ); - console.log('Gas used:', (await tx.wait()).gasUsed.toString()); - gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.INCH, (await tx.wait()).gasUsed); - }); - - it('uniswap', async function () { - const { - addr1, - uniswapUniversalRouter, - tokens, - settings: { gasUsedTableRow, amount }, - } = await loadFixture(initContractsWithCaseSettings); - - const uniswapV3Pool = await ethers.getContractAt('IUniswapV3Pool', UniswapV3Pools.USDC_DAI.address); - const slot0 = await uniswapV3Pool.slot0(); - const liquidity = await uniswapV3Pool.liquidity(); - - const uniswapV2Pool = await ethers.getContractAt('IUniswapV2Pair', UniswapV2Pools.WETH_DAI); - const reserves = await uniswapV2Pool.getReserves(); - - // for some reason the UniswapTrade SwapOptions constructor omits the "RouterSwapOptions" from the @uniswap/router-sdk - // so instead of making a `new SwapOptions` we must make an object instead - const options = { - slippageTolerance: new Percent('9999', '10000'), // 99.99% - }; - - const mixedRoute = new MixedRouteSDK( - [ - new Pair( - CurrencyAmount.fromRawAmount(new Token(1, tokens.DAI.target, 18), reserves.reserve0.toString()), - CurrencyAmount.fromRawAmount(new Token(1, tokens.WETH.target, 18), reserves.reserve1.toString()), - ), - new Pool( - new Token(1, tokens.USDC.target, 6), - new Token(1, tokens.DAI.target, 18), - UniswapV3Pools.USDC_DAI.fee, - slot0.sqrtPriceX96.toString(), - liquidity.toString(), - Number(slot0.tick), - ), - ], - new Ether(1), - new Token(1, tokens.USDC.target, 6), - ); - - const trade = MixedRouteTrade.createUncheckedTrade({ - route: mixedRoute, - inputAmount: CurrencyAmount.fromRawAmount(mixedRoute.input, amount.toString()), - outputAmount: CurrencyAmount.fromRawAmount(mixedRoute.output, '0'), - tradeType: TradeType.EXACT_INPUT, - }); - - // fix the object because building the unchecked trade doesn't create these fields - trade.routes = [mixedRoute]; - trade.swaps[0].route.protocol = Protocol.MIXED; - const uniswapTrade = new UniswapTrade(trade, options); - const { value, calldata } = SwapRouter.swapCallParameters(uniswapTrade); - - // finally addr1 can send the transaction - const tx = await addr1.sendTransaction({ - to: uniswapUniversalRouter.getAddress(), - value, - data: calldata, - }); - - console.log('Gas used:', (await tx.wait()).gasUsed.toString()); - gasUsedTable.addElementToRow(gasUsedTableRow, ProtocolKey.UNISWAP, (await tx.wait()).gasUsed); - }); - }); -}); diff --git a/test/helpers/utils.js b/test/helpers/utils.js index b013e5c..396a2e3 100644 --- a/test/helpers/utils.js +++ b/test/helpers/utils.js @@ -1,5 +1,10 @@ const { ethers } = require('hardhat'); const { trim0x } = require('@1inch/solidity-utils'); +const { MixedRouteTrade, MixedRouteSDK, Trade } = require('@uniswap/router-sdk'); +const { SwapRouter, UniswapTrade } = require('@uniswap/universal-router-sdk'); +const { Pool } = require('@uniswap/v3-sdk'); +const { Pair } = require('@uniswap/v2-sdk'); +const { CurrencyAmount, Token, TradeType, Ether, Percent } = require('@uniswap/sdk-core'); const ProtocolKey = { INCH: '1inch', @@ -31,8 +36,96 @@ function paraswapUniV2PoolData(pools) { return result; } +/** + * Create `value` and `data` for `uniswapUniversalRouter` contract to swap tokens through mixed pools. + * @param poolObjects Array of elements with pool data: + * - `string` with pool address gor UniswapV2 pools + * - `{ address: string, fee: number }` for UniswapV3 pools + * @param pathTokens Array of token addresses in the swap path + * @param useEth Use ETH as input/output token. 1 - input is eth, 2 - output is eth + */ +async function uniswapMixedPoolsData(poolObjects, pathTokens, useEth, amount) { + if (poolObjects.length + 1 !== pathTokens.length) { + return new Error('Invalid input data'); + } + + const chainId = 1; + const mixedRouteObjects = []; + let firstTokenDecimals = 18; + let lastTokenDecimals = 18; + for (let i = 0; i < poolObjects.length; i++) { + const poolObject = poolObjects[i]; + const fromTokenDecimals = Number(await (await ethers.getContractAt('IERC20Metadata', pathTokens[i])).decimals()); + const toTokenDecimals = Number(await (await ethers.getContractAt('IERC20Metadata', pathTokens[i + 1])).decimals()); + if (i === 0) firstTokenDecimals = fromTokenDecimals; + if (i === poolObjects.length - 2) lastTokenDecimals = toTokenDecimals; + + if (typeof poolObject === 'string') { + // UniswapV2 pools + const uniswapV2Pool = await ethers.getContractAt('IUniswapV2Pair', poolObject); + const reserves = await uniswapV2Pool.getReserves(); + const token0 = await uniswapV2Pool.token0(); + const [reserveFromToken, reserveToToken] = + token0 === pathTokens[i] ? [reserves.reserve0, reserves.reserve1] : [reserves.reserve1, reserves.reserve0]; + + mixedRouteObjects.push( + new Pair( + CurrencyAmount.fromRawAmount(new Token(chainId, pathTokens[i], fromTokenDecimals), reserveFromToken.toString()), + CurrencyAmount.fromRawAmount(new Token(chainId, pathTokens[i + 1], toTokenDecimals), reserveToToken.toString()), + ), + ); + } else { + // UniswapV3 pools + const uniswapV3Pool = await ethers.getContractAt('IUniswapV3Pool', poolObject.address); + const slot0 = await uniswapV3Pool.slot0(); + const liquidity = await uniswapV3Pool.liquidity(); + + mixedRouteObjects.push( + new Pool( + new Token(chainId, pathTokens[i], fromTokenDecimals), + new Token(chainId, pathTokens[i + 1], toTokenDecimals), + poolObject.fee, + slot0.sqrtPriceX96.toString(), + liquidity.toString(), + Number(slot0.tick), + ), + ); + } + } + + const mixedRoute = new MixedRouteSDK( + mixedRouteObjects, + useEth === 1 ? new Ether(chainId) : new Token(chainId, pathTokens[0], firstTokenDecimals), + useEth === 2 ? new Ether(chainId) : new Token(chainId, pathTokens[pathTokens.length - 1], lastTokenDecimals), + ); + + const mixedRouteTrade = MixedRouteTrade.createUncheckedTrade({ + route: mixedRoute, + inputAmount: CurrencyAmount.fromRawAmount(mixedRoute.input, amount.toString()), + outputAmount: CurrencyAmount.fromRawAmount(mixedRoute.output, '0'), + tradeType: TradeType.EXACT_INPUT, + }); + const trade = new Trade({ + v2Routes: [], + v3Routes: [], + mixedRoutes: [mixedRouteTrade] + .filter((t) => t instanceof MixedRouteTrade) + .map((t) => ({ + mixedRoute: t.route, + inputAmount: t.inputAmount, + outputAmount: t.outputAmount, + })), + tradeType: TradeType.EXACT_INPUT, + }); + + const uniswapTrade = new UniswapTrade(trade, { slippageTolerance: new Percent('5', '100') }); + const { value, calldata: data } = SwapRouter.swapCallParameters(uniswapTrade); + return { value, data }; +} + module.exports = { ProtocolKey, percentageOf, paraswapUniV2PoolData, + uniswapMixedPoolsData, };