import { BigNumber, ethers, PopulatedTransaction } from 'ethers'; import { ErrorCode } from '../error-handler/constants'; import { TransactionManagerRequest } from '../transaction-inputs/transaction-manager-request'; import { AlchemyApiConfig, GasOptimizationResult, TenderlyApiConfig } from '../types'; import { PERCENTAGE_INCREMENT_ON_GAS_LIMIT } from '../utils'; import { TransactionSimulator } from './tx-simulation'; export class GasLimitEstimate { private provider: ethers.providers.Provider; private transactionRequest: TransactionManagerRequest; private transaction: PopulatedTransaction; private gasEstimateIteration = 7; private gasEstimateMultiplier = 1.25; private txSimulator: TransactionSimulator; private defaultGasMultiplier = 1.25; constructor( provider: ethers.providers.Provider, transaction: PopulatedTransaction, transactionRequest: TransactionManagerRequest, gasMultiplier: number, tenderlyConfig: TenderlyApiConfig, alchemyConfig: AlchemyApiConfig, defaultGasMultiplier?: number, ) { this.provider = provider; this.transactionRequest = transactionRequest; this.transaction = transaction; this.gasEstimateMultiplier = gasMultiplier; this.txSimulator = new TransactionSimulator(tenderlyConfig, alchemyConfig); if (defaultGasMultiplier) { this.defaultGasMultiplier = defaultGasMultiplier; } } private getIncrementedValues(min: BigNumber, max: BigNumber): BigNumber[] { const values: BigNumber[] = []; let currentValue: BigNumber = min; // only collect next 5 increments on the gas limit while ( currentValue.lt(max) && values.length < this.gasEstimateIteration + 1 ) { // increase gas limit by 25% currentValue = currentValue.mul(BigNumber.from('125')).div(100); values.push(currentValue); } return values; } public async getOverallGasCaps(): Promise<{ gasLimitCap: BigNumber; gasLimitLow: BigNumber; higLimitValues: BigNumber[]; }> { const block = await this.provider.getBlock('pending'); // this is the cap of the gas limit const gasLimitCap = block.gasLimit; // this is the minimum gas limit const gasLimitLow = this.transaction.gasLimit || BigNumber.from(0); // calculate entire gas ranges from low to high in 50% breakup const higLimitValues = this.getIncrementedValues(gasLimitLow, gasLimitCap); return { gasLimitCap: higLimitValues[higLimitValues.length - 1], gasLimitLow, higLimitValues, }; } private hasInsufficientGasError(error: any): boolean { const errorMessage = error.message; return errorMessage ? errorMessage.includes('insufficient funds') || errorMessage.includes('Insufficient ether deposited') : false; } private async isExecutable( gasLimit: BigNumber, ): Promise<{ isExecutable: boolean; isInsufficientGas: boolean; isFunctionReverted: boolean; }> { this.transaction.gasLimit = gasLimit; try { // const response = await this.transactionRequest.ethCallOnTransaction( // this.transaction, // this.provider, // ); const response = await this.transactionRequest.callStaticTransaction( this.transaction, ) process.env.DEBUG == 'true' && console.log( '🚀 ~ file: gas-helper.ts:168 ~ GasOperations ~ executable ~ response:', response, ); return { isExecutable: true, isInsufficientGas: false, isFunctionReverted: false, }; } catch (error) { process.env.DEBUG == 'true' && console.log( '🚀 ~ file: gas-helper.ts:182 ~ GasOperations ~ executable ~ error:', error, ); if ( error?.error?.error?.code === -32000 || this.hasInsufficientGasError(error) ) { return { isExecutable: false, isInsufficientGas: true, isFunctionReverted: false, }; } else if (error.message && error.message.includes('out of gas')) { return { isExecutable: false, isInsufficientGas: true, isFunctionReverted: false, }; } else if (error?.code === ErrorCode.CALL_EXCEPTION) { return { isExecutable: false, isInsufficientGas: false, isFunctionReverted: true, }; } else { return { isExecutable: false, isInsufficientGas: false, isFunctionReverted: true, }; } } } private isCloseGasLimit(gasLow: BigNumber, gasHigh: BigNumber): boolean { // increase the gas price by 11% return gasLow.mul(110).div(100).lt(gasHigh); } private async estimateGasLimit( gasLimitLow: BigNumber, gasLimitHigh: BigNumber, higLimitValues: BigNumber[], gasLimitCap: BigNumber, counter: number, ) { // if there are no error then return const checkExecute = await this.isExecutable(gasLimitLow); if (checkExecute.isExecutable) { return gasLimitLow; } while (this.isCloseGasLimit(gasLimitLow, gasLimitHigh)) { const mid = gasLimitLow.add(gasLimitHigh).div(2); const { isExecutable, isInsufficientGas, isFunctionReverted, } = await this.isExecutable(mid); if (isInsufficientGas) { throw new Error('Insufficient gas'); } if (!isExecutable && !isFunctionReverted) { gasLimitHigh = mid; } else if (isFunctionReverted) { throw new Error('Function reverted'); } else { gasLimitLow = mid; return mid; } } // if gas limit is still not executable if (gasLimitHigh.gte(gasLimitCap)) { return gasLimitHigh; } // search on next 50% slot counter += 1; if (counter < higLimitValues.length) { return await this.estimateGasLimit( gasLimitHigh, higLimitValues[counter], higLimitValues, gasLimitCap, counter, ); } else { return gasLimitHigh; } } public doFloatMultiplication(gasLimit: BigNumber, gasMultiplier: number): BigNumber { const decimalAsString = (gasMultiplier * 10 ** 18).toString(); const multipliedGasLimit = gasLimit.mul(BigNumber.from(decimalAsString)); process.env.DEBUG == 'true' && console.log( '🚀 ~ file: gas-limit.ts:165 ~ GasLimitEstimate ~ applyMultiplier ~ multipliedGasLimit:', multipliedGasLimit, ); const value = parseInt(multipliedGasLimit.toString()) / 10 ** 18; process.env.DEBUG == 'true' && console.log( '🚀 ~ file: gas-limit.ts:171 ~ GasLimitEstimate ~ applyMultiplier ~ value:', value, ); const newGasLimit = BigNumber.from(parseInt(Math.ceil(value).toString())); process.env.DEBUG == 'true' && console.log( `🚀 ~ file: gas-limit.ts:174 ~ GasLimitEstimate ~ applyMultiplier ~ newGasLimit: ${newGasLimit}`, ); return newGasLimit; } public async applyMultiplier( gasLimit: BigNumber, defaultTransactionGasLimit: BigNumber, ): Promise { process.env.DEBUG == 'true' && console.log( '🚀 ~ file: gas-limit.ts:162 ~ GasLimitEstimate ~ applyMultiplier ~ gasLimit:', gasLimit, ); // highlight gasLimit on console console.log('=================GAS MULTIPLIER========================='); console.log( `====================${this.gasEstimateMultiplier}=====================`, ); console.log('========================================================'); process.env.DEBUG == 'true' && console.log( `🚀 ~ file: gas-limit.ts:162 ~ GasLimitEstimate ~ applyMultiplier ~ Gas limit ${this.gasEstimateMultiplier}:`, ); // performing decimal calculation let newGasLimit = await this.doFloatMultiplication(gasLimit, this.gasEstimateMultiplier); // get current base fee on the network const latestBlock = await this.provider.getBlock('latest'); const baseFee = latestBlock.baseFeePerGas; const simulationGasUsed = await this.txSimulator.simulate( this.transaction, baseFee, ); console.log( '🚀 ~ file: gas-limit.ts:236 ~ GasLimitEstimate ~ simulationGasUsed:', simulationGasUsed.toString(), ); console.log( '🚀 ~ file: gas-limit.ts:242 ~ GasLimitEstimate ~ newGasLimit:', newGasLimit.toString(), ); // check if gas is too lower then the expected const lowerGasLimitCap = this.doFloatMultiplication(defaultTransactionGasLimit, this.defaultGasMultiplier); console.log("🚀 ~ file: gas-limit.ts:274 ~ GasLimitEstimate ~ lowerGasLimitCap:", lowerGasLimitCap) if (newGasLimit.lt(lowerGasLimitCap)) { console.log("Setting up new gas limit as current went below desired limit"); newGasLimit = lowerGasLimitCap; } if (newGasLimit.lt(simulationGasUsed)) { newGasLimit = simulationGasUsed; // increase the gas limit by 15% newGasLimit = BigNumber.from( parseInt(newGasLimit.toString()) * (1 + PERCENTAGE_INCREMENT_ON_GAS_LIMIT), ); } const { isExecutable, isInsufficientGas, isFunctionReverted, } = await this.isExecutable(newGasLimit); if (isExecutable === false) { throw new Error('Gas limit is not executable'); } if (isFunctionReverted === true) { throw new Error('Function reverted'); } if (isInsufficientGas === true) { throw new Error('Insufficient gas'); } this.transaction.gasLimit = newGasLimit; process.env.DEBUG == 'true' && console.log( '=====================NEW GAS ESTIMATE=========================', ); process.env.DEBUG == 'true' && console.log( `====================${newGasLimit.toString()}=====================`, ); process.env.DEBUG == 'true' && console.log('========================================================'); return { gasLimit: newGasLimit, simulatedGasLimit: simulationGasUsed }; } public async getEstimate(defaultTransactionGasLimit: BigNumber): Promise { const { gasLimitCap, gasLimitLow, higLimitValues, } = await this.getOverallGasCaps(); // start binary search on first 50% slot const beginCounter = 0; const estimateGasFromStaticCall = await this.estimateGasLimit( gasLimitLow, higLimitValues[beginCounter], higLimitValues, gasLimitCap, beginCounter, ); process.env.DEBUG == 'true' && console.log( '🚀 ~ file: gas-limit.ts:197 ~ GasLimitEstimate ~ getEstimate ~ estimateGasFromStaticCall:', estimateGasFromStaticCall, ); // convert the multiplier to 10 ** 18 return this.applyMultiplier(estimateGasFromStaticCall, defaultTransactionGasLimit); } }