@sky-mavis/smart-order-router
Version:
Ronin Swap Smart Order Router
165 lines • 17.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.V3HeuristicGasModelFactory = void 0;
const bignumber_1 = require("@ethersproject/bignumber");
const sdk_core_1 = require("@uniswap/sdk-core");
const lodash_1 = __importDefault(require("lodash"));
const __1 = require("../../../..");
const amounts_1 = require("../../../../util/amounts");
const gas_factory_helpers_1 = require("../../../../util/gas-factory-helpers");
const log_1 = require("../../../../util/log");
const gas_model_1 = require("../gas-model");
const gas_costs_1 = require("./gas-costs");
/**
* Computes a gas estimate for a V3 swap using heuristics.
* Considers number of hops in the route, number of ticks crossed
* and the typical base cost for a swap.
*
* We get the number of ticks crossed in a swap from the QuoterV2
* contract.
*
* We compute gas estimates off-chain because
* 1/ Calling eth_estimateGas for a swaps requires the caller to have
* the full balance token being swapped, and approvals.
* 2/ Tracking gas used using a wrapper contract is not accurate with Multicall
* due to EIP-2929. We would have to make a request for every swap we wanted to estimate.
* 3/ For V2 we simulate all our swaps off-chain so have no way to track gas used.
*
* @export
* @class V3HeuristicGasModelFactory
*/
class V3HeuristicGasModelFactory extends gas_model_1.IOnChainGasModelFactory {
async buildGasModel({ chainId, gasPriceWei, pools, amountToken, quoteToken, providerConfig, }) {
const usdPool = pools.usdPool;
const calculateL1GasFees = async () => {
return await (0, gas_factory_helpers_1.calculateL1GasFeesHelper)(chainId, usdPool, quoteToken, pools.nativeAndQuoteTokenV3Pool);
};
const nativeCurrency = __1.WRAPPED_NATIVE_CURRENCY[chainId];
let nativeAmountPool = null;
if (!amountToken.equals(nativeCurrency)) {
nativeAmountPool = pools.nativeAndAmountTokenV3Pool;
}
const usdToken = usdPool.token0.equals(nativeCurrency) ? usdPool.token1 : usdPool.token0;
const estimateGasCost = (routeWithValidQuote) => {
var _a;
const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId, providerConfig);
/** ------ MARK: USD logic -------- */
const gasCostInTermsOfUSD = (0, gas_model_1.getQuoteThroughNativePool)(chainId, totalGasCostNativeCurrency, usdPool);
/** ------ MARK: Conditional logic run if gasToken is specified -------- */
const nativeAndSpecifiedGasTokenPool = pools.nativeAndSpecifiedGasTokenV3Pool;
let gasCostInTermsOfGasToken = undefined;
// we don't want to fetch the gasToken pool if the gasToken is the native currency
if (nativeAndSpecifiedGasTokenPool) {
gasCostInTermsOfGasToken = (0, gas_model_1.getQuoteThroughNativePool)(chainId, totalGasCostNativeCurrency, nativeAndSpecifiedGasTokenPool);
}
// if the gasToken is the native currency, we can just use the totalGasCostNativeCurrency
else if ((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken) === null || _a === void 0 ? void 0 : _a.equals(nativeCurrency)) {
gasCostInTermsOfGasToken = totalGasCostNativeCurrency;
}
/** ------ MARK: return early if quoteToken is wrapped native currency ------- */
if (quoteToken.equals(nativeCurrency)) {
return {
gasEstimate: baseGasUse,
gasCostInToken: totalGasCostNativeCurrency,
gasCostInUSD: gasCostInTermsOfUSD,
gasCostInGasToken: gasCostInTermsOfGasToken,
};
}
/** ------ MARK: Main gas logic in terms of quote token -------- */
// Since the quote token is not in the native currency, we convert the gas cost to be in terms of the quote token.
// We do this by getting the highest liquidity <quoteToken>/<nativeCurrency> pool. eg. <quoteToken>/ETH pool.
const nativeAndQuoteTokenPool = pools.nativeAndQuoteTokenV3Pool;
let gasCostInTermsOfQuoteToken = null;
if (nativeAndQuoteTokenPool) {
gasCostInTermsOfQuoteToken = (0, gas_model_1.getQuoteThroughNativePool)(chainId, totalGasCostNativeCurrency, nativeAndQuoteTokenPool);
}
// We may have a nativeAmountPool, but not a nativePool
else {
log_1.log.info(`Unable to find ${nativeCurrency.symbol} pool with the quote token, ${quoteToken.symbol} to produce gas adjusted costs. Using amountToken to calculate gas costs.`);
}
/** ------ MARK: (V3 ONLY) Logic for calculating synthetic gas cost in terms of amount token -------- */
// TODO: evaluate effectiveness and potentially refactor
// Highest liquidity pool for the non quote token / ETH
// A pool with the non quote token / ETH should not be required and errors should be handled separately
if (nativeAmountPool) {
// get current execution price (amountToken / quoteToken)
const executionPrice = new sdk_core_1.Price(routeWithValidQuote.amount.currency, routeWithValidQuote.quote.currency, routeWithValidQuote.amount.quotient, routeWithValidQuote.quote.quotient);
const inputIsToken0 = nativeAmountPool.token0.address == nativeCurrency.address;
// ratio of input / native
const nativeAndAmountTokenPrice = inputIsToken0 ? nativeAmountPool.token0Price : nativeAmountPool.token1Price;
const gasCostInTermsOfAmountToken = nativeAndAmountTokenPrice.quote(totalGasCostNativeCurrency);
// Convert gasCostInTermsOfAmountToken to quote token using execution price
const syntheticGasCostInTermsOfQuoteToken = executionPrice.quote(gasCostInTermsOfAmountToken);
// Note that the syntheticGasCost being lessThan the original quoted value is not always strictly better
// e.g. the scenario where the amountToken/ETH pool is very illiquid as well and returns an extremely small number
// however, it is better to have the gasEstimation be almost 0 than almost infinity, as the user will still receive a quote
if (gasCostInTermsOfQuoteToken === null ||
syntheticGasCostInTermsOfQuoteToken.lessThan(gasCostInTermsOfQuoteToken.asFraction)) {
log_1.log.info({
nativeAndAmountTokenPrice: nativeAndAmountTokenPrice.toSignificant(6),
gasCostInTermsOfQuoteToken: gasCostInTermsOfQuoteToken ? gasCostInTermsOfQuoteToken.toExact() : 0,
gasCostInTermsOfAmountToken: gasCostInTermsOfAmountToken.toExact(),
executionPrice: executionPrice.toSignificant(6),
syntheticGasCostInTermsOfQuoteToken: syntheticGasCostInTermsOfQuoteToken.toSignificant(6),
}, 'New gasCostInTermsOfQuoteToken calculated with synthetic quote token price is less than original');
gasCostInTermsOfQuoteToken = syntheticGasCostInTermsOfQuoteToken;
}
}
// If gasCostInTermsOfQuoteToken is null, both attempts to calculate gasCostInTermsOfQuoteToken failed (nativePool and amountNativePool)
if (gasCostInTermsOfQuoteToken === null) {
log_1.log.info(`Unable to find ${nativeCurrency.symbol} pool with the quote token, ${quoteToken.symbol}, or amount Token, ${amountToken.symbol} to produce gas adjusted costs. Route will not account for gas.`);
return {
gasEstimate: baseGasUse,
gasCostInToken: amounts_1.CurrencyAmount.fromRawAmount(quoteToken, 0),
gasCostInUSD: amounts_1.CurrencyAmount.fromRawAmount(usdToken, 0),
};
}
return {
gasEstimate: baseGasUse,
gasCostInToken: gasCostInTermsOfQuoteToken,
gasCostInUSD: gasCostInTermsOfUSD,
gasCostInGasToken: gasCostInTermsOfGasToken,
};
};
return {
estimateGasCost: estimateGasCost.bind(this),
calculateL1GasFees,
};
}
estimateGas(routeWithValidQuote, gasPriceWei, chainId, providerConfig) {
var _a;
const totalInitializedTicksCrossed = bignumber_1.BigNumber.from(Math.max(1, lodash_1.default.sum(routeWithValidQuote.initializedTicksCrossedList)));
const totalHops = bignumber_1.BigNumber.from(routeWithValidQuote.route.pools.length);
let hopsGasUse = gas_costs_1.COST_PER_HOP.mul(totalHops);
// We have observed that this algorithm tends to underestimate single hop swaps.
// We add a buffer in the case of a single hop swap.
if (totalHops.eq(1)) {
hopsGasUse = hopsGasUse.add(gas_costs_1.SINGLE_HOP_OVERHEAD);
}
// Some tokens have extremely expensive transferFrom functions, which causes
// us to underestimate them by a large amount. For known tokens, we apply an
// adjustment.
const tokenOverhead = gas_costs_1.TOKEN_OVERHEAD;
const tickGasUse = gas_costs_1.COST_PER_INIT_TICK.mul(totalInitializedTicksCrossed);
const uninitializedTickGasUse = gas_costs_1.COST_PER_UNINIT_TICK.mul(0);
// base estimate gas used based on chainId estimates for hops and ticks gas useage
const baseGasUse = gas_costs_1.BASE_SWAP_COST.add(hopsGasUse)
.add(tokenOverhead)
.add(tickGasUse)
.add(uninitializedTickGasUse)
.add((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.additionalGasOverhead) !== null && _a !== void 0 ? _a : bignumber_1.BigNumber.from(0));
const baseGasCostWei = gasPriceWei.mul(baseGasUse);
const wrappedCurrency = __1.WRAPPED_NATIVE_CURRENCY[chainId];
const totalGasCostNativeCurrency = amounts_1.CurrencyAmount.fromRawAmount(wrappedCurrency, baseGasCostWei.toString());
return {
totalGasCostNativeCurrency,
totalInitializedTicksCrossed,
baseGasUse,
};
}
}
exports.V3HeuristicGasModelFactory = V3HeuristicGasModelFactory;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidjMtaGV1cmlzdGljLWdhcy1tb2RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9yb3V0ZXJzL2FscGhhLXJvdXRlci9nYXMtbW9kZWxzL3YzL3YzLWhldXJpc3RpYy1nYXMtbW9kZWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsd0RBQXFEO0FBRXJELGdEQUEwQztBQUUxQyxvREFBdUI7QUFFdkIsbUNBQXNEO0FBQ3RELHNEQUEwRDtBQUMxRCw4RUFBZ0Y7QUFDaEYsOENBQTJDO0FBRTNDLDRDQU1zQjtBQUN0QiwyQ0FPcUI7QUFFckI7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBaUJHO0FBQ0gsTUFBYSwwQkFBMkIsU0FBUSxtQ0FBdUI7SUFDOUQsS0FBSyxDQUFDLGFBQWEsQ0FBQyxFQUN6QixPQUFPLEVBQ1AsV0FBVyxFQUNYLEtBQUssRUFDTCxXQUFXLEVBQ1gsVUFBVSxFQUNWLGNBQWMsR0FDa0I7UUFDaEMsTUFBTSxPQUFPLEdBQVMsS0FBSyxDQUFDLE9BQU8sQ0FBQztRQUVwQyxNQUFNLGtCQUFrQixHQUFHLEtBQUssSUFLN0IsRUFBRTtZQUNILE9BQU8sTUFBTSxJQUFBLDhDQUF3QixFQUFDLE9BQU8sRUFBRSxPQUFPLEVBQUUsVUFBVSxFQUFFLEtBQUssQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO1FBQ3ZHLENBQUMsQ0FBQztRQUVGLE1BQU0sY0FBYyxHQUFHLDJCQUF1QixDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ3hELElBQUksZ0JBQWdCLEdBQWdCLElBQUksQ0FBQztRQUN6QyxJQUFJLENBQUMsV0FBVyxDQUFDLE1BQU0sQ0FBQyxjQUFjLENBQUMsRUFBRTtZQUN2QyxnQkFBZ0IsR0FBRyxLQUFLLENBQUMsMEJBQTBCLENBQUM7U0FDckQ7UUFFRCxNQUFNLFFBQVEsR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLENBQUMsT0FBTyxD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