diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 61f132ae1..5bdaa91f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -313,8 +313,8 @@ jobs: TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} - integration-tests-quote-for-other-networks: - name: Integration Tests - Quote For Other Networks + integration-tests-quote-for-other-networks-mainnet: + name: Integration Tests - Quote For Other Networks Mainnet runs-on: ubuntu-latest steps: @@ -339,22 +339,322 @@ jobs: run: npm run build - name: Run Integration tests - run: npm run integ-test -- -t 'quote for other networks' + run: npm run integ-test -- -t 'quote for other networks * mainnet' env: JSON_RPC_PROVIDER: ${{ secrets.JSON_RPC_PROVIDER }} - JSON_RPC_PROVIDER_GORLI: ${{ secrets.JSON_RPC_PROVIDER_GORLI }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-optimism: + name: Integration Tests - Quote For Other Networks Optimism + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * optimism' + env: JSON_RPC_PROVIDER_OPTIMISM: ${{ secrets.JSON_RPC_PROVIDER_OPTIMISM }} JSON_RPC_PROVIDER_OPTIMISM_GOERLI: ${{ secrets.JSON_RPC_PROVIDER_OPTIMISM_GOERLI }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-arbitrum: + name: Integration Tests - Quote For Other Networks Arbitrum + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * arbitrum' + env: JSON_RPC_PROVIDER_ARBITRUM_ONE: ${{ secrets.JSON_RPC_PROVIDER_ARBITRUM_ONE }} JSON_RPC_PROVIDER_ARBITRUM_GOERLI: ${{ secrets.JSON_RPC_PROVIDER_ARBITRUM_GOERLI }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-polygon: + name: Integration Tests - Quote For Other Networks Polygon + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * polygon' + env: JSON_RPC_PROVIDER_POLYGON: ${{ secrets.JSON_RPC_PROVIDER_POLYGON }} JSON_RPC_PROVIDER_POLYGON_MUMBAI: ${{ secrets.JSON_RPC_PROVIDER_POLYGON_MUMBAI }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-goerli: + name: Integration Tests - Quote For Other Networks Goerli + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * goerli' + env: + JSON_RPC_PROVIDER_GORLI: ${{ secrets.JSON_RPC_PROVIDER_GORLI }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-celo: + name: Integration Tests - Quote For Other Networks Celo + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * celo' + env: JSON_RPC_PROVIDER_CELO: ${{ secrets.JSON_RPC_PROVIDER_CELO }} JSON_RPC_PROVIDER_CELO_ALFAJORES: ${{ secrets.JSON_RPC_PROVIDER_CELO_ALFAJORES }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-bnb: + name: Integration Tests - Quote For Other Networks BNB + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * bnb' + env: JSON_RPC_PROVIDER_BNB: ${{ secrets.JSON_RPC_PROVIDER_BNB }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-avalanche: + name: Integration Tests - Quote For Other Networks Avalanche + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * avalanche' + env: JSON_RPC_PROVIDER_AVALANCHE: ${{ secrets.JSON_RPC_PROVIDER_AVALANCHE }} + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-base: + name: Integration Tests - Quote For Other Networks Base + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * base' + env: JSON_RPC_PROVIDER_BASE: ${{ secrets.JSON_RPC_PROVIDER_BASE }} TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} TENDERLY_USER: ${{ secrets.TENDERLY_USER }} TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} + + integration-tests-quote-for-other-networks-remaining-networks: + name: Integration Tests - Quote For Other Networks Remaining Networks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + # This is required separately from yarn test because it generates the typechain definitions + - name: Compile + run: npm run build + + # This is to capture any new networks added into the integ-test suite + - name: Run Integration tests + run: npm run integ-test -- -t 'quote for other networks * (?!(mainnet|optimism|arbitrum|polygon|goerli|celo|bnb|avalanche|base))' + env: + # We don't know which new networks will be added, so we have no way to provider RPC URL ahead of time + # This will make remaining networks integ-test suite to fail, and dev is expected to manually add RPC URL for the new network + TENDERLY_BASE_URL: ${{ secrets.TENDERLY_BASE_URL }} + TENDERLY_USER: ${{ secrets.TENDERLY_USER }} + TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} + TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} diff --git a/package-lock.json b/package-lock.json index 0f31cbf5f..bd5dd3bcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.20.1", "license": "GPL", "dependencies": { + "@types/brotli": "^1.3.4", "@uniswap/default-token-list": "^11.2.0", "@uniswap/permit2-sdk": "^1.2.0", "@uniswap/router-sdk": "^1.6.0", @@ -22,6 +23,7 @@ "async-retry": "^1.3.1", "await-timeout": "^1.1.1", "axios": "^0.21.1", + "brotli": "^1.3.3", "bunyan": "^1.8.15", "bunyan-blackhole": "^1.1.1", "ethers": "^5.7.2", @@ -2750,6 +2752,14 @@ "@types/node": "*" } }, + "node_modules/@types/brotli": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/brotli/-/brotli-1.3.4.tgz", + "integrity": "sha512-cKYjgaS2DMdCKF7R0F5cgx1nfBYObN2ihIuPGQ4/dlIY6RpV7OWNwe9L8V4tTVKL2eZqOkNM9FM/rgTvLf4oXw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/bunyan": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.6.tgz", @@ -3975,6 +3985,14 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browser-level": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/browser-level/-/browser-level-1.0.1.tgz", @@ -13822,6 +13840,14 @@ "@types/node": "*" } }, + "@types/brotli": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/brotli/-/brotli-1.3.4.tgz", + "integrity": "sha512-cKYjgaS2DMdCKF7R0F5cgx1nfBYObN2ihIuPGQ4/dlIY6RpV7OWNwe9L8V4tTVKL2eZqOkNM9FM/rgTvLf4oXw==", + "requires": { + "@types/node": "*" + } + }, "@types/bunyan": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.6.tgz", @@ -14773,6 +14799,14 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "requires": { + "base64-js": "^1.1.2" + } + }, "browser-level": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/browser-level/-/browser-level-1.0.1.tgz", diff --git a/package.json b/package.json index aab319a33..c16aa2e89 100644 --- a/package.json +++ b/package.json @@ -31,19 +31,21 @@ "node": ">=10" }, "dependencies": { + "@types/brotli": "^1.3.4", "@uniswap/default-token-list": "^11.2.0", "@uniswap/permit2-sdk": "^1.2.0", "@uniswap/router-sdk": "^1.6.0", + "@uniswap/sdk-core": "^4.0.7", "@uniswap/swap-router-contracts": "^1.3.0", "@uniswap/token-lists": "^1.0.0-beta.31", "@uniswap/universal-router": "^1.0.1", "@uniswap/universal-router-sdk": "^1.5.8", "@uniswap/v2-sdk": "^3.2.3", "@uniswap/v3-sdk": "^3.10.0", - "@uniswap/sdk-core": "^4.0.7", "async-retry": "^1.3.1", "await-timeout": "^1.1.1", "axios": "^0.21.1", + "brotli": "^1.3.3", "bunyan": "^1.8.15", "bunyan-blackhole": "^1.1.1", "ethers": "^5.7.2", diff --git a/src/providers/v3/gas-data-provider.ts b/src/providers/v3/gas-data-provider.ts index 0ad95c74d..c076ca0b7 100644 --- a/src/providers/v3/gas-data-provider.ts +++ b/src/providers/v3/gas-data-provider.ts @@ -6,6 +6,7 @@ import { GasDataArbitrum__factory } from '../../types/other/factories/GasDataArb import { GasPriceOracle__factory } from '../../types/other/factories/GasPriceOracle__factory'; import { ARB_GASINFO_ADDRESS, log, OVM_GASPRICE_ADDRESS } from '../../util'; import { IMulticallProvider } from '../multicall-provider'; +import { ProviderConfig } from '../provider'; /** * Provider for getting gas constants on L2s. @@ -18,7 +19,7 @@ export interface IL2GasDataProvider { * Gets the data constants needed to calculate the l1 security fee on L2s like arbitrum and optimism. * @returns An object that includes the data necessary for the off chain estimations. */ - getGasData(): Promise; + getGasData(providerConfig?: ProviderConfig): Promise; } export type OptimismGasData = { @@ -49,7 +50,10 @@ export class OptimismGasDataProvider * @returns An OptimismGasData object that includes the l1BaseFee, * scalar, decimals, and overhead values. */ - public async getGasData(): Promise { + public async getGasData( + providerConfig?: ProviderConfig + ): Promise { + // TODO: Also get the gasPrice from GasPriceOracle.sol const funcNames = ['l1BaseFee', 'scalar', 'decimals', 'overhead']; const tx = await this.multicall2Provider.callMultipleFunctionsOnSameContract< @@ -59,6 +63,7 @@ export class OptimismGasDataProvider address: this.gasOracleAddress, contractInterface: GasPriceOracle__factory.createInterface(), functionNames: funcNames, + providerConfig: providerConfig, }); if ( @@ -115,12 +120,14 @@ export class ArbitrumGasDataProvider this.gasFeesAddress = gasDataAddress ? gasDataAddress : ARB_GASINFO_ADDRESS; } - public async getGasData() { + public async getGasData(providerConfig?: ProviderConfig) { const gasDataContract = GasDataArbitrum__factory.connect( this.gasFeesAddress, this.provider ); - const gasData = await gasDataContract.getPricesInWei(); + const gasData = await gasDataContract.getPricesInWei({ + blockTag: providerConfig?.blockNumber, + }); const perL1CalldataByte = gasData[1]; return { perL2TxFee: gasData[0], diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 2fbf9a693..d955f73b5 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -1504,7 +1504,7 @@ export class AlphaRouter // So we init a new CurrencyAmount object here CurrencyAmount.fromRawAmount(quoteCurrency, quote.quotient.toString()), this.l2GasDataProvider - ? await this.l2GasDataProvider!.getGasData() + ? await this.l2GasDataProvider!.getGasData(providerConfig) : undefined, providerConfig ); diff --git a/src/routers/alpha-router/functions/best-swap-route.ts b/src/routers/alpha-router/functions/best-swap-route.ts index 54c28d951..9b2fc75c4 100644 --- a/src/routers/alpha-router/functions/best-swap-route.ts +++ b/src/routers/alpha-router/functions/best-swap-route.ts @@ -441,6 +441,7 @@ export async function getBestSwapRouteBy( // if on L2, calculate the L1 security fee let gasCostsL1ToL2: L1ToL2GasCosts = { gasUsedL1: BigNumber.from(0), + gasUsedL1OnL2: BigNumber.from(0), gasCostL1USD: CurrencyAmount.fromRawAmount(usdToken, 0), gasCostL1QuoteToken: CurrencyAmount.fromRawAmount( // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain @@ -463,7 +464,7 @@ export async function getBestSwapRouteBy( } } - const { gasCostL1USD, gasCostL1QuoteToken } = gasCostsL1ToL2; + const { gasUsedL1OnL2, gasCostL1USD, gasCostL1QuoteToken } = gasCostsL1ToL2; // For each gas estimate, normalize decimals to that of the chosen usd token. const estimatedGasUsedUSDs = _(bestSwap) @@ -580,7 +581,7 @@ export async function getBestSwapRouteBy( return { quote, quoteGasAdjusted, - estimatedGasUsed, + estimatedGasUsed: estimatedGasUsed.add(gasUsedL1OnL2), estimatedGasUsedUSD, estimatedGasUsedQuoteToken, estimatedGasUsedGasToken, diff --git a/src/routers/alpha-router/gas-models/gas-model.ts b/src/routers/alpha-router/gas-models/gas-model.ts index b1efe6e7a..608c2f780 100644 --- a/src/routers/alpha-router/gas-models/gas-model.ts +++ b/src/routers/alpha-router/gas-models/gas-model.ts @@ -84,6 +84,7 @@ export const usdGasTokensByChain: { [chainId in ChainId]?: Token[] } = { export type L1ToL2GasCosts = { gasUsedL1: BigNumber; + gasUsedL1OnL2: BigNumber; gasCostL1USD: CurrencyAmount; gasCostL1QuoteToken: CurrencyAmount; }; diff --git a/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts b/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts index c7b187f94..5982f1dff 100644 --- a/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts +++ b/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts @@ -15,7 +15,7 @@ import { import { CurrencyAmount } from '../../../../util/amounts'; import { calculateArbitrumToL1FeeFromCalldata, - getL2ToL1GasUsed + getL2ToL1GasUsed, } from '../../../../util/gas-factory-helpers'; import { log } from '../../../../util/log'; import { @@ -75,7 +75,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { IGasModel > { const l2GasData = l2GasDataProvider - ? await l2GasDataProvider.getGasData() + ? await l2GasDataProvider.getGasData(providerConfig) : undefined; const usdPool: Pool = pools.usdPool; @@ -84,6 +84,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { route: V3RouteWithValidQuote[] ): Promise<{ gasUsedL1: BigNumber; + gasUsedL1OnL2: BigNumber; gasCostL1USD: CurrencyAmount; gasCostL1QuoteToken: CurrencyAmount; }> => { @@ -95,6 +96,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { }; let l1Used = BigNumber.from(0); let l1FeeInWei = BigNumber.from(0); + let gasUsedL1OnL2 = BigNumber.from(0); const opStackChains = [ ChainId.OPTIMISM, ChainId.OPTIMISM_GOERLI, @@ -105,17 +107,20 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { [l1Used, l1FeeInWei] = this.calculateOptimismToL1SecurityFee( route, swapOptions, - l2GasData as OptimismGasData + l2GasData as OptimismGasData, + chainId ); } else if ( chainId == ChainId.ARBITRUM_ONE || chainId == ChainId.ARBITRUM_GOERLI ) { - [l1Used, l1FeeInWei] = this.calculateArbitrumToL1SecurityFee( - route, - swapOptions, - l2GasData as ArbitrumGasData - ); + [l1Used, l1FeeInWei, gasUsedL1OnL2] = + this.calculateArbitrumToL1SecurityFee( + route, + swapOptions, + l2GasData as ArbitrumGasData, + chainId + ); } // wrap fee to native currency @@ -155,6 +160,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { // gasCostL1USD and gasCostL1QuoteToken is the cost of gas in each of those tokens return { gasUsedL1: l1Used, + gasUsedL1OnL2, gasCostL1USD, gasCostL1QuoteToken, }; @@ -386,7 +392,8 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { private calculateOptimismToL1SecurityFee( routes: V3RouteWithValidQuote[], swapConfig: SwapOptionsUniversalRouter, - gasData: OptimismGasData + gasData: OptimismGasData, + chainId: ChainId ): [BigNumber, BigNumber] { const { l1BaseFee, scalar, decimals, overhead } = gasData; @@ -407,22 +414,23 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { swapConfig, ChainId.OPTIMISM ).calldata; - const l1GasUsed = getL2ToL1GasUsed(data, overhead); + const l1GasUsed = getL2ToL1GasUsed(data, overhead, chainId); // l1BaseFee is L1 Gas Price on etherscan const l1Fee = l1GasUsed.mul(l1BaseFee); const unscaled = l1Fee.mul(scalar); // scaled = unscaled / (10 ** decimals) const scaledConversion = BigNumber.from(10).pow(decimals); const scaled = unscaled.div(scaledConversion); + // TODO: also return the gasUsedL1OnL2 because the final estimateGasUsed should include L1 calldata posting fee return [l1GasUsed, scaled]; } private calculateArbitrumToL1SecurityFee( routes: V3RouteWithValidQuote[], swapConfig: SwapOptionsUniversalRouter, - gasData: ArbitrumGasData - ): [BigNumber, BigNumber] { - + gasData: ArbitrumGasData, + chainId: ChainId + ): [BigNumber, BigNumber, BigNumber] { const route: V3RouteWithValidQuote = routes[0]!; const amountToken = @@ -441,6 +449,6 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { swapConfig, ChainId.ARBITRUM_ONE ).calldata; - return calculateArbitrumToL1FeeFromCalldata(data, gasData); + return calculateArbitrumToL1FeeFromCalldata(data, gasData, chainId); } } diff --git a/src/util/gas-factory-helpers.ts b/src/util/gas-factory-helpers.ts index 643aeeb88..5410b0201 100644 --- a/src/util/gas-factory-helpers.ts +++ b/src/util/gas-factory-helpers.ts @@ -3,6 +3,7 @@ import { Protocol } from '@uniswap/router-sdk'; import { ChainId, Token, TradeType } from '@uniswap/sdk-core'; import { Pair } from '@uniswap/v2-sdk/dist/entities'; import { FeeAmount, Pool } from '@uniswap/v3-sdk'; +import brotli from 'brotli'; import JSBI from 'jsbi'; import _ from 'lodash'; @@ -184,26 +185,46 @@ export function getGasCostInNativeCurrency( return costNativeCurrency; } +export function getArbitrumBytes(data: string): BigNumber { + if (data == '') return BigNumber.from(0); + const compressed = brotli.compress( + Buffer.from(data.replace('0x', ''), 'hex'), + { + mode: 0, + quality: 1, + lgwin: 22, + } + ); + // TODO: This is a rough estimate of the compressed size + // Brotli 0 should be used, but this brotli library doesn't support it + // https://github.com/foliojs/brotli.js/issues/38 + // There are other brotli libraries that do support it, but require async + // We workaround by using Brotli 1 with a 20% bump in size + return BigNumber.from(compressed.length).mul(120).div(100); +} + export function calculateArbitrumToL1FeeFromCalldata( calldata: string, - gasData: ArbitrumGasData -): [BigNumber, BigNumber] { - const { perL2TxFee, perL1CalldataFee } = gasData; + gasData: ArbitrumGasData, + chainId: ChainId +): [BigNumber, BigNumber, BigNumber] { + const { perL2TxFee, perL1CalldataFee, perArbGasTotal } = gasData; // calculates gas amounts based on bytes of calldata, use 0 as overhead. - const l1GasUsed = getL2ToL1GasUsed(calldata, BigNumber.from(0)); + const l1GasUsed = getL2ToL1GasUsed(calldata, BigNumber.from(0), chainId); // multiply by the fee per calldata and add the flat l2 fee - let l1Fee = l1GasUsed.mul(perL1CalldataFee); - l1Fee = l1Fee.add(perL2TxFee); - return [l1GasUsed, l1Fee]; + const l1Fee = l1GasUsed.mul(perL1CalldataFee).add(perL2TxFee); + const gasUsedL1OnL2 = l1Fee.div(perArbGasTotal); + return [l1GasUsed, l1Fee, gasUsedL1OnL2]; } export function calculateOptimismToL1FeeFromCalldata( calldata: string, - gasData: OptimismGasData + gasData: OptimismGasData, + chainId: ChainId ): [BigNumber, BigNumber] { const { l1BaseFee, scalar, decimals, overhead } = gasData; - const l1GasUsed = getL2ToL1GasUsed(calldata, overhead); + const l1GasUsed = getL2ToL1GasUsed(calldata, overhead, chainId); // l1BaseFee is L1 Gas Price on etherscan const l1Fee = l1GasUsed.mul(l1BaseFee); const unscaled = l1Fee.mul(scalar); @@ -213,23 +234,42 @@ export function calculateOptimismToL1FeeFromCalldata( return [l1GasUsed, scaled]; } -// based on the code from the optimism OVM_GasPriceOracle contract -export function getL2ToL1GasUsed(data: string, overhead: BigNumber): BigNumber { - // data is hex encoded - const dataArr: string[] = data.slice(2).match(/.{1,2}/g)!; - const numBytes = dataArr.length; - let count = 0; - for (let i = 0; i < numBytes; i += 1) { - const byte = parseInt(dataArr[i]!, 16); - if (byte == 0) { - count += 4; - } else { - count += 16; +export function getL2ToL1GasUsed( + data: string, + overhead: BigNumber, + chainId: ChainId +): BigNumber { + switch (chainId) { + case ChainId.ARBITRUM_ONE: + case ChainId.ARBITRUM_GOERLI: { + // calculates bytes of compressed calldata + const l1ByteUsed = getArbitrumBytes(data); + return l1ByteUsed.mul(16); + } + case ChainId.OPTIMISM: + case ChainId.OPTIMISM_GOERLI: + case ChainId.BASE: + case ChainId.BASE_GOERLI: { + // based on the code from the optimism OVM_GasPriceOracle contract + // data is hex encoded + const dataArr: string[] = data.slice(2).match(/.{1,2}/g)!; + const numBytes = dataArr.length; + let count = 0; + for (let i = 0; i < numBytes; i += 1) { + const byte = parseInt(dataArr[i]!, 16); + if (byte == 0) { + count += 4; + } else { + count += 16; + } + } + const unsigned = overhead.add(count); + const signedConversion = 68 * 16; + return unsigned.add(signedConversion); } + default: + return BigNumber.from(0); } - const unsigned = overhead.add(count); - const signedConversion = 68 * 16; - return unsigned.add(signedConversion); } export async function calculateGasUsed( @@ -262,7 +302,8 @@ export async function calculateGasUsed( ) { l2toL1FeeInWei = calculateOptimismToL1FeeFromCalldata( route.methodParameters!.calldata, - l2GasData as OptimismGasData + l2GasData as OptimismGasData, + chainId )[1]; } diff --git a/test/integ/routers/alpha-router/alpha-router.integration.test.ts b/test/integ/routers/alpha-router/alpha-router.integration.test.ts index 6fb255020..fcf66516c 100644 --- a/test/integ/routers/alpha-router/alpha-router.integration.test.ts +++ b/test/integ/routers/alpha-router/alpha-router.integration.test.ts @@ -644,7 +644,12 @@ describe('alpha router integration', () => { v2PoolProvider, v3PoolProvider, hardhat.providers[0]!, - portionProvider + portionProvider, + { + // Tenderly team has fixed all the nuances post Arbitrum nitro update, + // so we can use the gas limits returned from Tenderly for more accurate L2 gas estimate assertions. + [ChainId.ARBITRUM_ONE]: 1 + }, ); const simulator = new FallbackTenderlySimulator( @@ -1556,7 +1561,7 @@ describe('alpha router integration', () => { tradeType == TradeType.EXACT_INPUT ? parseAmount('100', tokenIn) : parseAmount('100', tokenOut); - + const swap = await alphaRouter.route( amount, getQuoteToken(tokenIn, tokenOut, tradeType), @@ -1572,17 +1577,17 @@ describe('alpha router integration', () => { gasToken: DAI_MAINNET.address } ); - + expect(swap).toBeDefined(); expect(swap).not.toBeNull(); - + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken } = swap!; - + expect(estimatedGasUsedGasToken).toBeDefined(); expect(estimatedGasUsedGasToken?.currency.equals(DAI_MAINNET)).toBe(true); - + await validateSwapRoute(quote, quoteGasAdjusted, tradeType, 100, 10); - + await validateExecuteSwap( SwapType.UNIVERSAL_ROUTER, quote, @@ -1594,7 +1599,7 @@ describe('alpha router integration', () => { 100 ); }); - + it('erc20 -> eth gas token as weth', async () => { // declaring these to reduce confusion const tokenIn = USDC_MAINNET; @@ -1603,7 +1608,7 @@ describe('alpha router integration', () => { tradeType == TradeType.EXACT_INPUT ? parseAmount('1000000', tokenIn) : parseAmount('10', tokenOut); - + const swap = await alphaRouter.route( amount, getQuoteToken(tokenIn, tokenOut, tradeType), @@ -1619,17 +1624,17 @@ describe('alpha router integration', () => { gasToken: WRAPPED_NATIVE_CURRENCY[1]!.address } ); - + expect(swap).toBeDefined(); expect(swap).not.toBeNull(); - + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken } = swap!; - + expect(estimatedGasUsedGasToken).toBeDefined(); expect(estimatedGasUsedGasToken?.currency.equals(WRAPPED_NATIVE_CURRENCY[1]!)).toBe(true); - + await validateSwapRoute(quote, quoteGasAdjusted, tradeType); - + await validateExecuteSwap( SwapType.UNIVERSAL_ROUTER, quote, @@ -2516,7 +2521,7 @@ describe('alpha router integration', () => { tradeType == TradeType.EXACT_INPUT ? parseAmount('100', tokenIn) : parseAmount('100', tokenOut); - + const swap = await alphaRouter.route( amount, getQuoteToken(tokenIn, tokenOut, tradeType), @@ -2533,19 +2538,19 @@ describe('alpha router integration', () => { gasToken: DAI_MAINNET.address } ); - + expect(swap).toBeDefined(); expect(swap).not.toBeNull(); - + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken, simulationStatus } = swap!; expect(simulationStatus).toBeDefined(); expect(simulationStatus).toEqual(SimulationStatus.Succeeded); expect(estimatedGasUsedGasToken).toBeDefined(); expect(estimatedGasUsedGasToken?.currency.equals(DAI_MAINNET)).toBe(true); - + await validateSwapRoute(quote, quoteGasAdjusted, tradeType, 100, 10); - + await validateExecuteSwap( SwapType.UNIVERSAL_ROUTER, quote, @@ -2557,7 +2562,7 @@ describe('alpha router integration', () => { 100 ); }); - + it('erc20 -> eth gas token as weth', async () => { // declaring these to reduce confusion const tokenIn = USDC_MAINNET; @@ -2566,7 +2571,7 @@ describe('alpha router integration', () => { tradeType == TradeType.EXACT_INPUT ? parseAmount('1000000', tokenIn) : parseAmount('10', tokenOut); - + const swap = await alphaRouter.route( amount, getQuoteToken(tokenIn, tokenOut, tradeType), @@ -2583,19 +2588,19 @@ describe('alpha router integration', () => { gasToken: WRAPPED_NATIVE_CURRENCY[1]!.address } ); - + expect(swap).toBeDefined(); expect(swap).not.toBeNull(); - + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken, simulationStatus } = swap!; expect(simulationStatus).toBeDefined(); expect(simulationStatus).toEqual(SimulationStatus.Succeeded); expect(estimatedGasUsedGasToken).toBeDefined(); expect(estimatedGasUsedGasToken?.currency.equals(WRAPPED_NATIVE_CURRENCY[1]!)).toBe(true); - + await validateSwapRoute(quote, quoteGasAdjusted, tradeType); - + await validateExecuteSwap( SwapType.UNIVERSAL_ROUTER, quote, @@ -3541,7 +3546,7 @@ describe('quote for other networks', () => { : parseAmount('10', tokenOut); // Universal Router is not deployed on Gorli. - const swapOptions: SwapOptions = + const swapWithSimulationOptions: SwapOptions = chain == ChainId.GOERLI ? { type: SwapType.SWAP_ROUTER_02, @@ -3558,11 +3563,11 @@ describe('quote for other networks', () => { simulate: { fromAddress: WHALES(tokenIn) }, }; - const swap = await alphaRouter.route( + const swapWithSimulation = await alphaRouter.route( amount, getQuoteToken(tokenIn, tokenOut, tradeType), tradeType, - swapOptions, + swapWithSimulationOptions, { // @ts-ignore[TS7053] - complaining about switch being non exhaustive ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], @@ -3570,17 +3575,62 @@ describe('quote for other networks', () => { saveTenderlySimulationIfFailed: true, } ); - expect(swap).toBeDefined(); - expect(swap).not.toBeNull(); - if (swap) { + expect(swapWithSimulation).toBeDefined(); + expect(swapWithSimulation).not.toBeNull(); + + // TODO: We need to fix L2 -> L1 calldata posting gas estimate one by one. + // We started by fixing Arbitrum gas estimate https://github.com/Uniswap/smart-order-router/pull/468. + // Before the fix, the non-simulated gas estimate is 131000, meanwhile simulated gas estimate is 6573602. + // After the fix, the non-simulated gas estimate is now 3414207. + // We assert the non-simulated gas estimate is within 50% error margin of simulated gas estimate range, e.g. between 6573602 - 6573602 / 2 and 6573602 + 6573602 / 2 + if (chain === ChainId.ARBITRUM_ONE) { + // Universal Router is not deployed on Gorli. + const swapOptions: SwapOptions = + { + type: SwapType.UNIVERSAL_ROUTER, + recipient: WHALES(tokenIn), + slippageTolerance: SLIPPAGE, + deadlineOrPreviousBlockhash: parseDeadline(360), + }; + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + swapOptions, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.V3, Protocol.V2], + saveTenderlySimulationIfFailed: true, + } + ); + + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const gasEstimateDiff = swapWithSimulation!.estimatedGasUsed.gt(swap!.estimatedGasUsed) + ? swapWithSimulation!.estimatedGasUsed.sub(swap!.estimatedGasUsed) + : swap!.estimatedGasUsed.sub(swapWithSimulation!.estimatedGasUsed); + + // We will rely on Tenderly gas estimate as source of truth against SOR non-simulated gas estimate accuracy. + // This is the only reliable and long-term feasible test assertion approach. + // For example, in the near future, after EIP-4844, we expect the gas estimate to drop by (3 / 16) + // due to gas cost per compressed calldata byte dropping from 16 to 3. + // Relying on Tenderly gas estimate is the only way our github CI can auto catch this. + const percentDiff = gasEstimateDiff.mul(BigNumber.from(100)).div(swapWithSimulation!.estimatedGasUsed); + expect(percentDiff.lte(BigNumber.from(50))).toBe(true); + } + + if (swapWithSimulation) { expect( - swap.quoteGasAdjusted - .subtract(swap.quote) - .equalTo(swap.estimatedGasUsedQuoteToken) + swapWithSimulation.quoteGasAdjusted + .subtract(swapWithSimulation.quote) + .equalTo(swapWithSimulation.estimatedGasUsedQuoteToken) ); // Expect tenderly simulation to be successful - expect(swap.simulationStatus).toEqual( + expect(swapWithSimulation.simulationStatus).toEqual( SimulationStatus.Succeeded ); } diff --git a/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts b/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts index 0cc72f9f4..efc335ebc 100644 --- a/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts +++ b/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts @@ -9,12 +9,14 @@ import { V3PoolProvider, V3Route, V3RouteWithValidQuote, - WRAPPED_NATIVE_CURRENCY, + WRAPPED_NATIVE_CURRENCY } from '../../../../../src'; import { calculateGasUsed, + getArbitrumBytes, getHighestLiquidityV3NativePool, getHighestLiquidityV3USDPool, + getL2ToL1GasUsed, } from '../../../../../src/util/gas-factory-helpers'; import { buildMockV3PoolAccessor, @@ -26,7 +28,7 @@ import { } from '../../../../test-util/mock-data'; import { BigNumber } from 'ethers'; import { getMockedV2PoolProvider, getMockedV3PoolProvider } from '../gas-models/test-util/mocked-dependencies'; -import { TradeType } from '@uniswap/sdk-core'; +import { ChainId, TradeType } from '@uniswap/sdk-core'; import { Trade } from '@uniswap/router-sdk'; import { Route } from '@uniswap/v3-sdk'; import { getPools } from '../gas-models/test-util/helpers'; @@ -211,4 +213,15 @@ describe('gas factory helpers tests', () => { expect(quoteGasAdjustedArb.equalTo(quoteGasAdjusted)).toBe(true); }) }) + + describe('getL2ToL1GasUsed', () => { + for (const chainId of [ChainId.ARBITRUM_ONE]) { + it('should return the gas costs for the compressed bytes', async () => { + const calldata = '0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000003fc10e65473c5939c700000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b82af49447d8a07e3bd95bd0d56f35241523fbab1000bb8912ce59144191c1204e64559fe8253a0e49e6548000000000000000000000000000000000000000000'; + const compressedBytes = getArbitrumBytes(calldata); + const gasUsed = getL2ToL1GasUsed(calldata, BigNumber.from(0), chainId); + expect(gasUsed).toEqual(compressedBytes.mul(16)); + }); + } + }) });