Skip to content

Commit

Permalink
fix: gas price estimates for swaps (#1251)
Browse files Browse the repository at this point in the history
Co-authored-by: Pano Skylakis <[email protected]>
  • Loading branch information
keithbro-imx and pano-skylakis authored Dec 6, 2023
1 parent ce3898b commit d8fd065
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import {
TEST_FROM_ADDRESS,
expectToBeString,
decodeMulticallExactInputWithoutFees,
buildBlock,
TEST_MAX_PRIORITY_FEE_PER_GAS,
TEST_BASE_FEE,
} from './test/utils';

jest.mock('@ethersproject/providers');
Expand All @@ -58,7 +61,7 @@ jest.mock('./lib/utils', () => ({

const HIGHER_SLIPPAGE = 0.2;
const APPROVED_AMOUNT = newAmountFromString('1', USDC_TEST_TOKEN);
const APPROVE_GAS_ESTIMATE = BigNumber.from('100000');
const APPROVE_GAS_ESTIMATE = BigNumber.from('100000'); // gas units

describe('getUnsignedSwapTxFromAmountIn', () => {
let erc20Contract: jest.Mock<any, any, any>;
Expand All @@ -75,9 +78,14 @@ describe('getUnsignedSwapTxFromAmountIn', () => {
})) as unknown as JsonRpcProvider;

(JsonRpcBatchProvider as unknown as jest.Mock).mockImplementation(() => ({
getFeeData: async () => ({
maxFeePerGas: null,
gasPrice: TEST_GAS_PRICE,
getBlock: async () => buildBlock({ baseFeePerGas: BigNumber.from(TEST_BASE_FEE) }),
send: jest.fn().mockImplementation(async (method) => {
switch (method) {
case 'eth_maxPriorityFeePerGas':
return BigNumber.from(TEST_MAX_PRIORITY_FEE_PER_GAS);
default:
throw new Error('Method not implemented');
}
}),
})) as unknown as JsonRpcProvider;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
setupSwapTxTest,
TEST_ROUTER_ADDRESS,
TEST_DEX_CONFIGURATION,
TEST_GAS_PRICE,
TEST_FEE_RECIPIENT,
TEST_SECONDARY_FEE_ADDRESS,
decodeMulticallExactOutputSingleWithFees,
Expand All @@ -36,6 +35,9 @@ import {
expectToBeString,
refundETHFunctionSignature,
NATIVE_TEST_TOKEN,
buildBlock,
TEST_BASE_FEE,
TEST_MAX_PRIORITY_FEE_PER_GAS,
} from './test/utils';

jest.mock('@ethersproject/providers');
Expand Down Expand Up @@ -68,9 +70,14 @@ describe('getUnsignedSwapTxFromAmountOut', () => {
}));

(JsonRpcBatchProvider as unknown as jest.Mock).mockImplementation(() => ({
getFeeData: async () => ({
maxFeePerGas: null,
gasPrice: TEST_GAS_PRICE,
getBlock: async () => buildBlock({ baseFeePerGas: BigNumber.from(TEST_BASE_FEE) }),
send: jest.fn().mockImplementation(async (method) => {
switch (method) {
case 'eth_maxPriorityFeePerGas':
return BigNumber.from(TEST_MAX_PRIORITY_FEE_PER_GAS); // 10 gwei
default:
throw new Error('Method not implemented');
}
}),
})) as unknown as JsonRpcProvider;
});
Expand Down
82 changes: 29 additions & 53 deletions packages/internal/dex/sdk/src/lib/transactionUtils/gas.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { JsonRpcProvider } from '@ethersproject/providers';
import { BigNumber } from '@ethersproject/bignumber';
import { expectToBeDefined, NATIVE_TEST_TOKEN, TEST_CHAIN_ID, TEST_RPC_URL } from 'test/utils';
import { buildBlock, expectToBeDefined, formatAmount, NATIVE_TEST_TOKEN } from 'test/utils';
import { newAmount } from 'lib/utils';
import { BigNumber, providers } from 'ethers';
import { IMMUTABLE_TESTNET_RPC_URL, IMMUTABLE_TESTNET_CHAIN_ID } from 'constants/chains';
import { calculateGasFee, fetchGasPrice } from './gas';

jest.mock('@ethersproject/providers');

describe('calculateGasFee', () => {
describe('when given a price and gas used', () => {
it('calculates gas fee from gas used and gas price', async () => {
Expand All @@ -21,63 +19,41 @@ describe('calculateGasFee', () => {
});

describe('fetchGasPrice', () => {
describe('when no fee data is returned', () => {
it('should return null', async () => {
(JsonRpcProvider as unknown as jest.Mock).mockImplementation(() => ({
getFeeData: async () => ({
maxFeePerGas: null,
gasPrice: null,
}),
})) as unknown as JsonRpcProvider;

const provider = new JsonRpcProvider(TEST_RPC_URL, TEST_CHAIN_ID);

const gasFeeEstimate = await fetchGasPrice(provider, NATIVE_TEST_TOKEN);

expect(gasFeeEstimate).toBeNull();
describe.skip('for realsies', () => {
it('returns a gasPriceEstimate', async () => {
const provider = new providers.JsonRpcProvider(IMMUTABLE_TESTNET_RPC_URL, IMMUTABLE_TESTNET_CHAIN_ID);
const gasPriceEstimate = await fetchGasPrice(provider, NATIVE_TEST_TOKEN);
expectToBeDefined(gasPriceEstimate);
expect(formatAmount(gasPriceEstimate)).toEqual('0.000000010000000098');
});
});

describe('when EIP-1559 is not supported', () => {
it('should return the gasPrice', async () => {
const gasPrice = BigNumber.from('1500000000'); // 1.5 gwei or 1500000000 wei

(JsonRpcProvider as unknown as jest.Mock).mockImplementation(() => ({
getFeeData: async () => ({
maxFeePerGas: null,
gasPrice,
}),
})) as unknown as JsonRpcProvider;

const provider = new JsonRpcProvider(TEST_RPC_URL, TEST_CHAIN_ID);

const gasFeeEstimate = await fetchGasPrice(provider, NATIVE_TEST_TOKEN);
describe('when no fee data is returned', () => {
it('should return null', async () => {
const provider = {
getBlock: jest.fn().mockRejectedValue(new Error('failed to get block')),
send: jest.fn().mockRejectedValue(new Error('failed to get maxPriorityFeePerGas')),
};

expectToBeDefined(gasFeeEstimate);
expect(gasFeeEstimate.value.toString()).toEqual('1500000000');
expect(gasFeeEstimate.token.type).toEqual('native');
const gasPriceEstimate = await fetchGasPrice(provider, NATIVE_TEST_TOKEN);
expect(gasPriceEstimate).toBeNull();
});
});

describe('when EIP-1559 is supported', () => {
it('should return the maxFeePerGas', async () => {
const maxFeePerGas = BigNumber.from('2500000000'); // 2.5 gwei or 2500000000 wei
const maxPriorityFeePerGas = BigNumber.from('500000000'); // 0.5 gwei or 500000000 wei

(JsonRpcProvider as unknown as jest.Mock).mockImplementation(() => ({
getFeeData: async () => ({
maxFeePerGas,
maxPriorityFeePerGas,
gasPrice: null,
}),
})) as unknown as JsonRpcProvider;

const provider = new JsonRpcProvider(TEST_RPC_URL, TEST_CHAIN_ID);

const gasFeeEstimate = await fetchGasPrice(provider, NATIVE_TEST_TOKEN);
expectToBeDefined(gasFeeEstimate);
expect(gasFeeEstimate.value.toString()).toEqual('3000000000');
expect(gasFeeEstimate.token.type).toEqual('native');
const lastBaseFeePerGas = BigNumber.from('49'); // 49 wei
const maxPriorityFeePerGas = BigNumber.from('500000000'); // 0.5 gwei

const provider = {
getBlock: async () => buildBlock({ baseFeePerGas: lastBaseFeePerGas }),
send: jest.fn().mockResolvedValueOnce(maxPriorityFeePerGas),
};

const gasPriceEstimate = await fetchGasPrice(provider, NATIVE_TEST_TOKEN);
expectToBeDefined(gasPriceEstimate);
expect(gasPriceEstimate.value.toString()).toEqual('500000098'); // maxPriorityFeePerGas + 2 * lastBaseFeePerGas
expect(gasPriceEstimate.token.type).toEqual('native');
});
});
});
52 changes: 28 additions & 24 deletions packages/internal/dex/sdk/src/lib/transactionUtils/gas.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
import { BigNumber } from 'ethers';
import { JsonRpcProvider, FeeData } from '@ethersproject/providers';
import { BigNumber, providers } from 'ethers';
import { newAmount } from 'lib';
import { CoinAmount, Native } from 'types';

type EIP1559FeeData = {
type FeeData = {
maxFeePerGas: BigNumber;
maxPriorityFeePerGas: BigNumber;
lastBaseFeePerGas: BigNumber;
gasPrice: null;
};

/**
* Determines whether or not the chain supports EIP-1559 by checking for the existence
* of {@link FeeData.maxFeePerGas} and {@link FeeData.maxPriorityFeePerGas}
*
* @param {FeeData} fee - The fee data for the chain
*/
export const doesChainSupportEIP1559 = (fee: FeeData): fee is EIP1559FeeData => {
const supportsEIP1559 = !!fee.maxFeePerGas && !!fee.maxPriorityFeePerGas;
return supportsEIP1559;
interface Provider {
getBlock(blockHashOrBlockTag: string): Promise<providers.Block>
send(method: string, params: Array<any>): Promise<any>
}

const getFeeData = async (provider: Provider): Promise<FeeData> => {
const [block, maxPriorityFeePerGas] = await Promise.all([
provider.getBlock('latest'),
provider.send('eth_maxPriorityFeePerGas', []) as Promise<BigNumber>,
]);

if (!block.baseFeePerGas) throw new Error('Base fee per gas not found in block');

return {
// https://www.blocknative.com/blog/eip-1559-fees
maxFeePerGas: block.baseFeePerGas.mul(2).add(maxPriorityFeePerGas),
maxPriorityFeePerGas,
lastBaseFeePerGas: block.baseFeePerGas,
};
};

/**
* Fetch the current gas price estimate. Supports both EIP-1559 and non-EIP1559 chains
* @param {JsonRpcProvider} provider - The JSON RPC provider used to fetch fee data
* @param {Provider} provider - The JSON RPC provider used to fetch fee data
* @param {Native} nativeToken - The native token of the chain. Gas prices will be denominated in this token
* @returns {Amount | null} - The gas price in the smallest denomination of the chain's currency,
* @returns {CoinAmount<Native> | null} - The gas price in the smallest denomination of the chain's currency,
* or null if no gas price is available
*/
export const fetchGasPrice = async (provider: JsonRpcProvider, nativeToken: Native)
: Promise<CoinAmount<Native> | null> => {
const feeData = await provider.getFeeData().catch(() => null);
// eslint-disable-next-line max-len
export const fetchGasPrice = async (provider: Provider, nativeToken: Native): Promise<CoinAmount<Native> | null> => {
const feeData = await getFeeData(provider).catch(() => null);
if (!feeData) return null;

if (doesChainSupportEIP1559(feeData)) {
return newAmount(feeData.maxFeePerGas.add(feeData.maxPriorityFeePerGas), nativeToken);
}

return feeData.gasPrice ? newAmount(feeData.gasPrice, nativeToken) : null;
return newAmount(feeData.maxFeePerGas, nativeToken);
};

/**
* Calculate the gas fee from the gas price and gas units used for the transaction
*
* @param {Amount} gasPrice - The price of gas
* @param {CoinAmount<Native>} gasPrice - The price of gas
* @param {BigNumber} gasEstimate - The total gas units that will be used for the transaction
* @returns - The cost of the transaction in the gas token's smallest denomination (e.g. WEI)
*/
Expand Down
23 changes: 21 additions & 2 deletions packages/internal/dex/sdk/src/test/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TradeType } from '@uniswap/sdk-core';
import { BigNumber, BigNumberish, utils } from 'ethers';
import { BigNumber, BigNumberish, utils, providers } from 'ethers';
import { Pool, Route, TickMath } from '@uniswap/v3-sdk';
import { SwapRouter } from '@uniswap/router-sdk';
import { Environment, ImmutableConfiguration } from '@imtbl/config';
Expand All @@ -11,7 +11,9 @@ import { NativeTokenService } from 'lib/nativeTokenService';
import { ExchangeModuleConfiguration, SecondaryFee, CoinAmount, Coin, ERC20, Native, Amount } from 'types';
import { erc20ToUniswapToken, newAmount, Router, RoutingContracts } from '../lib';

export const TEST_GAS_PRICE = BigNumber.from('1500000000'); // 1.5 gwei or 1500000000 wei
export const TEST_BASE_FEE = BigNumber.from('49'); // 49 wei
export const TEST_MAX_PRIORITY_FEE_PER_GAS = BigNumber.from('10000000000'); // 10 gwei
export const TEST_GAS_PRICE = BigNumber.from('10000000098'); // TEST_MAX_PRIORITY_FEE_PER_GAS + 2 * TEST_BASE_FEE
export const TEST_TRANSACTION_GAS_USAGE = BigNumber.from('200000'); // 200,000 gas units

export const TEST_CHAIN_ID = 999;
Expand Down Expand Up @@ -519,3 +521,20 @@ export function newAmountFromString<T extends Coin>(amount: string, token: T): C
const bn = utils.parseUnits(amount, token.decimals);
return newAmount(bn, token);
}

export const buildBlock = ({ baseFeePerGas }: { baseFeePerGas: BigNumber | null }): providers.Block => ({
baseFeePerGas,
// eslint-disable-next-line @typescript-eslint/naming-convention
_difficulty: BigNumber.from('0'),
difficulty: 0,
extraData: '',
gasLimit: BigNumber.from('0'),
gasUsed: BigNumber.from('0'),
hash: '',
miner: '',
nonce: '',
number: 0,
parentHash: '',
timestamp: 0,
transactions: [],
});

0 comments on commit d8fd065

Please sign in to comment.