Use ethers.js V6 and simplify functions #1

Closed
tornadocontrib wants to merge 5 commits from tornadocontrib/tornado-oracles:simple into main
11 changed files with 351 additions and 722 deletions
Showing only changes of commit 3271e88b13 - Show all commits

View File

@ -1,106 +0,0 @@
import { LegacyGasPrices } from './types';
export enum ChainId {
MAINNET = 1,
GOERLI = 5,
BSC = 56,
XDAI = 100,
POLYGON = 137,
OPTIMISM = 10,
ARBITRUM = 42161,
AVAX = 43114,
}
export enum InstanceTokenSymbol {
DAI = 'dai',
cDAI = 'cdai',
WBTC = 'wbtc',
USDT = 'usdt',
USDC = 'usdc',
}
export type GasPricesConfig = {
[chainId in ChainId]: LegacyGasPrices;
};
export const defaultGasPrices: GasPricesConfig = {
[ChainId.MAINNET]: {
instant: 80,
fast: 50,
standard: 25,
low: 8,
},
[ChainId.GOERLI]: {
instant: 80,
fast: 50,
standard: 25,
low: 8,
},
[ChainId.OPTIMISM]: {
instant: 0.001,
fast: 0.001,
standard: 0.001,
low: 0.001,
},
[ChainId.XDAI]: {
instant: 6,
fast: 5,
standard: 4,
low: 1,
},
[ChainId.BSC]: {
instant: 5,
fast: 4,
standard: 3,
low: 3,
},
[ChainId.POLYGON]: {
instant: 100,
fast: 75,
standard: 50,
low: 30,
},
[ChainId.ARBITRUM]: {
instant: 4,
fast: 3,
standard: 2.52,
low: 2.29,
},
[ChainId.AVAX]: {
instant: 225,
fast: 35,
standard: 25,
low: 25,
},
};
type GasLimitConfig = {
[chainId in ChainId]: number;
};
export const defaultWithdrawalGasLimit: GasLimitConfig = {
[ChainId.MAINNET]: 550000,
[ChainId.GOERLI]: 550000,
[ChainId.ARBITRUM]: 1900000,
[ChainId.OPTIMISM]: 440000,
[ChainId.AVAX]: 390000,
[ChainId.BSC]: 390000,
[ChainId.POLYGON]: 390000,
[ChainId.XDAI]: 390000,
};
type InstanceTokenGasLimitConfig = {
[tokenSymbol in InstanceTokenSymbol]: number;
};
export const defaultInstanceTokensGasLimit: InstanceTokenGasLimitConfig = {
[InstanceTokenSymbol.DAI]: 55000,
[InstanceTokenSymbol.cDAI]: 425000,
[InstanceTokenSymbol.WBTC]: 85000,
[InstanceTokenSymbol.USDT]: 100000,
[InstanceTokenSymbol.USDC]: 80000,
};
export const optimismL1FeeOracleAddress = '0x420000000000000000000000000000000000000F';
export const offchainOracleAddress = '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb';
export const multiCallAddress = '0xda3c19c6fe954576707fa24695efb830d9cca1ca';

18
src/constants.ts Normal file
View File

@ -0,0 +1,18 @@
export enum ChainId {
MAINNET = 1,
BSC = 56,
XDAI = 100,
POLYGON = 137,
OPTIMISM = 10,
ARBITRUM = 42161,
AVAX = 43114,
SEPOLIA = 11155111,
}
export type ChainIdType = ChainId | number;
export const ENS_CHAINS = [ChainId.MAINNET, ChainId.SEPOLIA];
export const multiCallAddress = '0xcA11bde05977b3631167028862bE2a173976CA11';
export const POLYGON_GAS_STATION = 'https://gasstation.polygon.technology/v2';

View File

@ -1,279 +1,117 @@
import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { BigNumber, BigNumberish, ethers } from 'ethers';
import BigNumberFloat from 'bignumber.js';
import { parseUnits } from 'ethers/lib/utils';
import {
TransactionData,
TxType,
ITornadoFeeOracle,
LegacyGasPriceKey,
GasPriceParams,
GetGasParamsRes,
HexadecimalStringifiedNumber,
GetGasInput,
GetGasParamsInput,
} from './types';
import { JsonRpcProvider } from '@ethersproject/providers';
import { ChainId, defaultGasPrices, defaultInstanceTokensGasLimit, InstanceTokenSymbol } from './config';
import { bump, calculateGasPriceInWei, convertETHToToken, fromGweiToWeiHex, serializeTx } from './utils';
import { getOptimismL1FeeOracle } from './contracts/factories';
import { GetWithdrawalFeeViaRelayerInput } from './types';
import { AvailableTokenSymbols } from '@tornado/tornado-config';
import { Transaction, parseUnits } from 'ethers';
import type { BigNumberish, TransactionLike } from 'ethers';
import { OptimismL1FeeOracle, OptimismL1FeeOracle__factory } from './contracts';
import { ChainIdType } from './constants';
import { getProvider } from './providers';
import { networkConfig } from './types';
import { convertETHToTokenAmount } from './utils';
export abstract class TornadoFeeOracle implements ITornadoFeeOracle {
protected provider: JsonRpcProvider;
const DUMMY_ADDRESS = '0x1111111111111111111111111111111111111111';
public constructor(
public version: 4 | 5,
protected chainId: ChainId,
rpcUrl: string,
protected oracle: GasPriceOracle,
) {
this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
}
const DUMMY_NONCE = '0x1111111111111111111111111111111111111111111111111111111111111111';
/**
* Because Optimism transaction published on Mainnet, for each OP transaction we need to calculate L1 security fee:
* https://community.optimism.io/docs/developers/build/transaction-fees/#priority-fee
* @param {TransactionData} [tx] Transaction data to estimate L1 additional fee
* @returns {Promise<HexadecimalStringifiedNumber>} Fee in WEI (MATIC), '0' if chain is not Optimism
*/
async fetchL1OptimismFee(tx?: TransactionData): Promise<HexadecimalStringifiedNumber> {
if (this.chainId != ChainId.OPTIMISM) return BigNumber.from(0).toHexString();
const DUMMY_WITHDRAW_DATA =
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111';
const optimismL1FeeOracle = getOptimismL1FeeOracle(this.provider);
const l1Fee = await optimismL1FeeOracle.getL1Fee(serializeTx(tx));
export interface RelayerFeeParams {
gasPrice: BigNumberish;
gasLimit?: BigNumberish;
l1Fee?: BigNumberish;
denomination: BigNumberish;
ethRefund: BigNumberish;
tokenPriceInWei: BigNumberish;
tokenDecimals: number;
relayerFeePercent?: number;
isEth?: boolean;
premiumPercent?: number;
}
return l1Fee.toHexString();
}
export class TornadoFeeOracle {
optimismL1FeeOracle?: OptimismL1FeeOracle;
/**
* Estimate gas price, gas limit and l1Fee for sidechain (if exists)
* @param {GetGasParamsInput} [params] Function input arguments object
* @param {TransactionData} [params.tx] Transaction data in web3 / ethers format
* @param {TxType} [params.txType=other] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other'
* @param {number} [params.predefinedGasLimit] Predefined gas limit, if already calculated (no refetching)
* @param {number} [params.predefinedGasPrice] Predefined gas price, if already calculated (no refetching)
* @param {number} [params.bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (if gas limit not predefined, recenlty used)
* @param {number} [params.bumpGasPricePercent] Gas price bump percent to prioritize transaction (if gas limit not predefined, rarely used)
* @param {LegacyGasPriceKey} [params.speed] Preferred transaction speed, if uses legacy gas (before EIP-1559)
* @param {boolean} [params.includeL1FeeToGasLimit=true] Include L1 additional fee on Optimism to gas limit (get fee and divide by gas price)
* @returns {Promise<GetGasParamsRes>} Object with fields 'gasPrice' and 'gasLimit', L1 fee, if exists, included in gasLimit
*/
async getGasParams(params: GetGasParamsInput = {}): Promise<GetGasParamsRes> {
let {
tx,
txType = 'other',
bumpGasLimitPercent,
bumpGasPricePercent,
predefinedGasLimit: gasLimit,
predefinedGasPrice: gasPrice,
speed,
includeL1FeeToGasLimit = true,
} = params;
constructor(netId: ChainIdType, rpcUrl: string, config: networkConfig) {
const { optimismL1FeeOracleAddress } = config;
let l1Fee: string = '0';
if (!gasLimit && !gasPrice) {
[gasPrice, gasLimit, l1Fee] = await Promise.all([
this.getGasPrice(speed, bumpGasPricePercent),
this.getGasLimit(tx, txType, bumpGasLimitPercent),
this.fetchL1OptimismFee(tx),
]);
}
if (!gasLimit) {
[gasLimit, l1Fee] = await Promise.all([
this.getGasLimit(tx, txType, bumpGasLimitPercent),
this.fetchL1OptimismFee(tx),
]);
}
if (!gasPrice) gasPrice = await this.getGasPrice(speed, bumpGasPricePercent);
if (includeL1FeeToGasLimit)
// Include L1 fee in gas limit (divide by gas price before), if l1 fee is 0, gas limit wont change
gasLimit = BigNumberFloat(gasLimit)
.plus(BigNumberFloat(l1Fee).div(BigNumberFloat(gasPrice)))
.decimalPlaces(0, 1)
.toNumber();
return { gasLimit, gasPrice };
}
/**
* Estimates next block gas for signed, unsigned or incomplete Tornado transaction
* @param {GetGasInput} [params] Function input arguments object
* @param {TransactionData} [params.tx] Transaction data in web3 / ethers format
* @param {TxType} [params.txType] Tornado transaction type: withdrawal by user, withdrawal by relayer or 'other'
* @param {number} [params.predefinedGasLimit] Predefined gas limit, if already calculated (no refetching)
* @param {number} [params.predefinedGasPrice] Predefined gas price, if already calculated (no refetching)
* @param {number} [params.bumpGasLimitPercent] Gas limit bump percent to prioritize transaction (if gas limit not predefined, recenlty used)
* @param {number} [params.bumpGasPricePercent] Gas price bump percent to prioritize transaction (if gas price not predefined, rarely used)
* @param {LegacyGasPriceKey} [params.speed] Preferred transaction speed, if uses legacy gas (before EIP-1559)
* @returns {Promise<HexadecimalStringifiedNumber>} Gas value in WEI (hex-format)
*/
async getGas(params: GetGasInput = {}): Promise<HexadecimalStringifiedNumber> {
const { gasPrice, gasLimit } = await this.getGasParams({ ...params, includeL1FeeToGasLimit: true });
return BigNumber.from(gasPrice).mul(gasLimit).toHexString();
}
/**
* Estimate next block gas price
* @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559)
* @param {number} [bumpPercent=0] Gas bump percent to prioritize transaction
* @returns {Promise<GasPriceParams>} Estimated gas price info in WEI (hexed) - legacy object with gasPrice property or
* EIP-1559 object with maxFeePerGas and maxPriorityFeePerGas properties
*/
async getGasPriceParams(speed?: LegacyGasPriceKey, bumpPercent: number = 0): Promise<GasPriceParams> {
// Use instant for BSC, because on this chain "fast" and "instant" differs more than third and "fast" transaction can take hours
if (!speed) speed = this.chainId === ChainId.BSC ? 'instant' : 'fast';
try {
return await this.oracle.getTxGasParams({ legacySpeed: speed, bumpPercent });
} catch (e) {
return { gasPrice: bump(fromGweiToWeiHex(defaultGasPrices[this.chainId][speed]), bumpPercent).toHexString() };
if (optimismL1FeeOracleAddress) {
const provider = getProvider(netId, rpcUrl, config);
this.optimismL1FeeOracle = OptimismL1FeeOracle__factory.connect(optimismL1FeeOracleAddress, provider);
}
}
/**
* Estimate next block gas price
* @param {LegacyGasPriceKey} [speed] Preferred transaction speed, if uses legacy gas (before EIP-1559)
* @param {number} [bumpPercent] Gas bump percent to prioritize transaction
* @returns {Promise<HexadecimalStringifiedNumber>} Gas price in WEI (hex string)
*/
async getGasPrice(speed?: LegacyGasPriceKey, bumpPercent?: number): Promise<HexadecimalStringifiedNumber> {
const gasPriceParams = await this.getGasPriceParams(speed, bumpPercent);
return calculateGasPriceInWei(gasPriceParams).toHexString();
}
/**
* Estimates gas limit for transaction (or basic gas limit, if no tx data provided)
* @param {TransactionData} [tx] Transaction data (object in web3 / ethers format)
* @param {TxType} [type] Tornado transaction type: withdrawal by user, withdrawal by relayer, relayer fee check or 'other'
* @param {number} [bumpPercent] Gas bump percent to prioritize transaction
* @returns {Promise<number>} Gas limit
*/
abstract getGasLimit(tx?: TransactionData, type?: TxType, bumpPercent?: number): Promise<number>;
/**
* If user withdraw non-native tokens on ETH or Goerli, we need to calculate refund value:
* if the withdrawal is successful, this amount will be returned to the user after the transfer to the relayer,
* and if the relayer pays a commission and the transfer of tokens fails, this commission will remain to the relayer.
* Calculate L1 fee for op-stack chains
*
* Refund needed that recipient can use tokens after withdrawal (covers gas fee for send/swap)
* @param {BigNumberish} gasPrice Actual gas price
* @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai'
* @returns {HexadecimalStringifiedNumber} Refund amount in WEI (in hex format)
* This is required since relayers would pay the full transaction fees for users
*/
calculateRefundInETH(gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol): HexadecimalStringifiedNumber {
// Refund only available for non-native tokens on Ethereum Mainnet and Goerli
if (![ChainId.MAINNET, ChainId.GOERLI].includes(this.chainId) || (tokenSymbol as AvailableTokenSymbols) === 'eth')
return '0';
// Notify user about error if incorrect token symbol provided
if (!Object.values(InstanceTokenSymbol).includes(tokenSymbol)) {
console.error(
`Invalid token symbol: ${tokenSymbol}, must be lowercase token from one of Tornado ETH Mainnet pools`,
);
return '0';
fetchL1OptimismFee(tx?: TransactionLike): Promise<bigint> {
if (!this.optimismL1FeeOracle) {
return new Promise((resolve) => resolve(BigInt(0)));
}
// In Tornado we need to calculate refund only on user side, relayer get refund value in proof
const gasLimit = defaultInstanceTokensGasLimit[tokenSymbol];
return BigNumber.from(gasPrice).mul(gasLimit).mul(2).toHexString();
}
/**
* Fetched actual gas price and calculates refund amount
* @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai'
* @returns {Promise<HexadecimalStringifiedNumber>} Refund amount in WEI (in hex format)
*/
async fetchRefundInETH(tokenSymbol: InstanceTokenSymbol): Promise<HexadecimalStringifiedNumber> {
const gasPrice = await this.getGasPrice();
return this.calculateRefundInETH(gasPrice, tokenSymbol);
}
/**
* Get refund amount on ETH or Goerli in non-native token
* @param {BigNumberish} gasPrice Actual gas price in ETH
* @param {BigNumberish} tokenPriceInEth Token price in WEI in ETH
* @param {HexadecimalStringifiedNumber | number} tokenDecimals Token (currency) decimals
* @param {InstanceTokenSymbol} tokenSymbol Withdrawal token (currency) symbol - for example, 'dai'
* @returns {HexadecimalStringifiedNumber} Refund amount in WEI in selected token (hexed number)
*/
calculateRefundInToken(
gasPrice: BigNumberish,
tokenPriceInEth: BigNumberish,
tokenDecimals: HexadecimalStringifiedNumber | number,
tokenSymbol: InstanceTokenSymbol,
): HexadecimalStringifiedNumber {
const refundInEth = this.calculateRefundInETH(gasPrice, tokenSymbol);
return convertETHToToken(refundInEth, tokenDecimals, tokenPriceInEth).toHexString();
}
/**
* Calculates relayer fee in selected currency (ETH, DAI, BNB etc) in WEI
* @param {number | string} relayerFeePercent Relayer percent (0.4 for ETH Mainnet, for example)
* @param {HexadecimalStringifiedNumber | number} amount Amount in selected currency (10 for 10 ETH, 1000 for 1000 DAI)
* @param {string | number} decimals Decimal places in selected token (currency)
* @returns {HexadecimalStringifiedNumber} Fee in WEI (hexed stingified number)
*/
calculateRelayerFeeInWei(
relayerFeePercent: number | string,
amount: HexadecimalStringifiedNumber | number,
decimals: string | number,
): HexadecimalStringifiedNumber {
return parseUnits(amount.toString(), decimals)
.mul(`${Math.floor(Number(relayerFeePercent) * 1e10)}`)
.div(`${100 * 1e10}`)
.toHexString();
}
/**
* Estimates fee for withdrawal via relayer depending on type: gas bump percent is bigger, if it calculates by user,
* so that the real commission from the relayer side is a little less,
* in order to the relayer can send a transaction without fear that he will go into the red
* @param {GetWithdrawalFeeViaRelayerInput} params Function input arguments object
* @param {TxType} params.txType Tornado transaction type: withdrawal costs calculation from user side or from relayer side
* @param {TransactionData} [params.tx] Transaction data (object in web3 / ethers format)
* @param {number} params.relayerFeePercent Relayer fee percent from the transaction amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet)
* @param {AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>} params.currency Currency symbol
* @param {number | HexadecimalStringifiedNumber } params.amount Withdrawal amount in selected currency
* @param {number | HexadecimalStringifiedNumber } params.decimals Token (currency) decimals
* @param {BigNumberish} [params.refundInEth] Refund in ETH, if withdrawed other tokens on Mainnet (not ETH). Can not be provided, if user-side calculation
* @param {BigNumberish} [params.tokenPriceInEth] If withdrawing other token on Mainnet or Goerli, need to provide token price in ETH (in WEI)
* @param {number} [params.gasLimit] Predefined gas limit, if already calculated (no refetching)
* @param {number} [params.gasPrice] Predefined gas price, if already calculated (no refetching)
*
* @returns {Promise<HexadecimalStringifiedNumber>} Fee in WEI (hexed string)
*/
async calculateWithdrawalFeeViaRelayer({
tx,
txType,
relayerFeePercent,
currency,
amount,
decimals,
refundInEth,
tokenPriceInEth,
predefinedGasLimit,
predefinedGasPrice,
}: GetWithdrawalFeeViaRelayerInput): Promise<HexadecimalStringifiedNumber> {
const relayerFee = this.calculateRelayerFeeInWei(relayerFeePercent, amount, decimals);
const { gasPrice, gasLimit } = await this.getGasParams({ tx, txType, predefinedGasLimit, predefinedGasPrice });
const gasCosts = BigNumber.from(gasPrice).mul(gasLimit);
if ((this.chainId === ChainId.MAINNET || this.chainId === ChainId.GOERLI) && currency.toLowerCase() != 'eth') {
if (!tokenPriceInEth) {
console.error('Token price is required argument, if not native chain token is withdrawed');
return '0';
}
if (txType === 'user_withdrawal' && refundInEth === undefined)
refundInEth = this.calculateRefundInETH(gasPrice, currency.toLowerCase() as InstanceTokenSymbol);
const feeInEth = BigNumber.from(gasCosts).add(refundInEth || 0);
return convertETHToToken(feeInEth, decimals, tokenPriceInEth).add(relayerFee).toHexString();
if (!tx) {
// this tx is only used to simulate bytes size of the encoded tx so has nothing to with the accuracy
// inspired by the old style classic-ui calculation
tx = {
type: 0,
gasLimit: 1_000_000,
nonce: Number(DUMMY_NONCE),
data: DUMMY_WITHDRAW_DATA,
gasPrice: parseUnits('1', 'gwei'),
from: DUMMY_ADDRESS,
to: DUMMY_ADDRESS,
};
}
return BigNumber.from(gasCosts).add(relayerFee).toHexString();
return this.optimismL1FeeOracle.getL1Fee.staticCall(Transaction.from(tx).unsignedSerialized);
}
/**
* We don't need to distinguish default refunds by tokens since most users interact with other defi protocols after withdrawal
* So we default with 1M gas which is enough for two or three swaps
* Using 30 gwei for default but it is recommended to supply cached gasPrice value from the UI
*/
defaultEthRefund(gasPrice?: BigNumberish, gasLimit?: BigNumberish): bigint {
return (gasPrice ? BigInt(gasPrice) : parseUnits('30', 'gwei')) * BigInt(gasLimit || 1_000_000);
}
/**
* Calculates token amount for required ethRefund purchases required to calculate fees
*/
calculateTokenAmount(ethRefund: BigNumberish, tokenPriceInEth: BigNumberish, tokenDecimals?: number): bigint {
return convertETHToTokenAmount(ethRefund, tokenPriceInEth, tokenDecimals);
}
/**
* Warning: For tokens you need to check if the fees are above denomination
* (Usually happens for small denomination pool or if the gas price is high)
*/
calculateRelayerFee({
gasPrice,
gasLimit = 600_000,
l1Fee = 0,
denomination,
ethRefund = BigInt(0),
tokenPriceInWei,
tokenDecimals = 18,
relayerFeePercent = 0.33,
isEth = true,
premiumPercent = 20,
}: RelayerFeeParams): bigint {
const gasCosts = BigInt(gasPrice) * BigInt(gasLimit) + BigInt(l1Fee);
const relayerFee = (BigInt(denomination) * BigInt(Math.floor(10000 * relayerFeePercent))) / BigInt(10000 * 100);
if (isEth) {
// Add 20% premium
return ((gasCosts + relayerFee) * BigInt(premiumPercent ? 100 + premiumPercent : 100)) / BigInt(100);
}
const feeInEth = gasCosts + BigInt(ethRefund);
return (
((convertETHToTokenAmount(feeInEth, tokenPriceInWei, tokenDecimals) + relayerFee) *
BigInt(premiumPercent ? 100 + premiumPercent : 100)) /
BigInt(100)
);
}
}

View File

@ -1,36 +0,0 @@
import { defaultWithdrawalGasLimit } from './config';
import { TornadoFeeOracle } from './feeOracle';
import { ITornadoFeeOracle, TransactionData, TxType, LegacyGasPrices } from './types';
import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { bump } from './utils';
/**
* Oracle for V4 (old-version) transactions - estimates fee with predefined gas limit and without smart bumping
*/
export class TornadoFeeOracleV4 extends TornadoFeeOracle implements ITornadoFeeOracle {
public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) {
const oracleConfig = {
chainId,
defaultRpc: rpcUrl,
defaultFallbackGasPrices: defaultGasPrices,
};
const gasPriceOracle = new GasPriceOracle(oracleConfig);
super(4, chainId, rpcUrl, gasPriceOracle);
}
async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 0): Promise<number> {
if (type === 'user_withdrawal') return bump(defaultWithdrawalGasLimit[this.chainId], bumpPercent).toNumber();
// Need to bump relayer gas limit for transaction, because predefined gas limit to small to be 100% sure that transaction will be sent
// This leads to fact that relayer often pays extra for gas from his own funds, however, this was designed by previous developers
if (type === 'relayer_withdrawal')
return bump(defaultWithdrawalGasLimit[this.chainId], bumpPercent || 25).toNumber();
// For compatibility reasons, when wee check user-provided fee for V4 withdrawal transaction, we need dump gas limit
// for about 20 percent,so that the transaction will be sent, even if it results in some loss for the relayer
if (type === 'relayer_withdrawal_check_v4') return bump(defaultWithdrawalGasLimit[this.chainId], -25).toNumber();
if (!tx || Object.keys(tx).length === 0) return bump(23_000, bumpPercent).toNumber();
return bump(await this.provider.estimateGas(tx), bumpPercent).toNumber();
}
}

View File

@ -1,74 +0,0 @@
import { ChainId } from './config';
import { TornadoFeeOracle } from './feeOracle';
import {
ITornadoFeeOracle,
TransactionData,
TxType,
LegacyGasPrices,
LegacyGasPriceKey,
GasPriceParams,
} from './types';
import { GasPriceOracle } from '@tornado/gas-price-oracle';
import { bump } from './utils';
import { TornadoFeeOracleV4 } from './feeOracleV4';
/**
* Oracle for new V5 version - estimates transaction fees with smart gas limit & bumping
*/
export class TornadoFeeOracleV5 extends TornadoFeeOracle implements ITornadoFeeOracle {
private fallbackFeeOracle: TornadoFeeOracleV4;
public constructor(chainId: number, rpcUrl: string, defaultGasPrices?: LegacyGasPrices) {
const oracleConfig = {
chainId,
defaultRpc: rpcUrl,
minPriority: chainId === ChainId.MAINNET || chainId === ChainId.GOERLI ? 2 : chainId === ChainId.BSC ? 3 : 0.05,
percentile: 5,
blocksCount: 20,
defaultFallbackGasPrices: defaultGasPrices,
};
const gasPriceOracle = new GasPriceOracle(oracleConfig);
super(5, chainId, rpcUrl, gasPriceOracle);
this.fallbackFeeOracle = new TornadoFeeOracleV4(chainId, rpcUrl, defaultGasPrices);
}
async getGasLimit(tx?: TransactionData, type: TxType = 'other', bumpPercent: number = 20): Promise<number> {
if (!tx || Object.keys(tx).length === 0) return this.fallbackFeeOracle.getGasLimit(tx, type, bumpPercent);
/* Relayer gas limit must be lower so that fluctuations in gas price cannot lead to the fact that
* the relayer will actually pay for gas more than the money allocated for this by the user
* (that is, in fact, relayer will pay for gas from his own money, unless we make the bump percent less for him)
*/
if (type === 'relayer_withdrawal') bumpPercent = 10;
if (type === 'user_withdrawal') bumpPercent = 30;
try {
const fetchedGasLimit = await this.provider.estimateGas(tx);
return bump(fetchedGasLimit, bumpPercent).toNumber();
} catch (e) {
return this.fallbackFeeOracle.getGasLimit(tx, type, bumpPercent);
}
}
async getGasPriceParams(speed?: LegacyGasPriceKey, bumpPercent?: number): Promise<GasPriceParams> {
// Only if bump percent didn't provided (if user provides 0, no need to recalculate)
if (bumpPercent === undefined) {
switch (this.chainId) {
case ChainId.GOERLI:
bumpPercent = 100;
break;
case ChainId.POLYGON:
case ChainId.AVAX:
case ChainId.XDAI:
bumpPercent = 30;
break;
default:
bumpPercent = 10;
}
}
return super.getGasPriceParams(speed, bumpPercent);
}
}

View File

@ -1,3 +1,7 @@
export * from './feeOracleV4';
export * from './feeOracleV5';
export * from './constants';
export * from './feeOracle';
export * from './multicall';
export * from './providers';
export * from './tokenPriceOracle';
export * from './types';
export * from './utils';

28
src/multicall.ts Normal file
View File

@ -0,0 +1,28 @@
import { Interface } from 'ethers';
import { Multicall } from './contracts';
import type { Call3 } from './types';
export async function multicall(Multicall: Multicall, calls: Call3[]) {
const calldata = calls.map((call) => {
const target = (call.contract?.target || call.address) as string;
const callInterface = (call.contract?.interface || call.interface) as Interface;
return {
target,
callData: callInterface.encodeFunctionData(call.name, call.params),
allowFailure: call.allowFailure ?? false,
};
});
const returnData = await Multicall.aggregate3.staticCall(calldata);
const res = returnData.map((call, i) => {
const callInterface = (calls[i].contract?.interface || calls[i].interface) as Interface;
const [result, data] = call;
const decodeResult =
result && data && data !== '0x' ? callInterface.decodeFunctionResult(calls[i].name, data) : null;
return !decodeResult ? null : decodeResult.length === 1 ? decodeResult[0] : decodeResult;
});
return res;
}

107
src/providers.ts Normal file
View File

@ -0,0 +1,107 @@
import { JsonRpcProvider, Network, parseUnits, FetchUrlFeeDataNetworkPlugin, EnsPlugin, GasCostPlugin } from 'ethers';
import { GasPriceOracle, GasPriceOracle__factory, Multicall, Multicall__factory } from './contracts';
import { ENS_CHAINS, ChainIdType, multiCallAddress, POLYGON_GAS_STATION } from './constants';
import { multicall } from './multicall';
import type { networkConfig, gasOracleOptions } from './types';
import { isNode } from './utils';
// caching to improve performance
const oracleMapper = new Map();
const multicallMapper = new Map();
export function getGasOraclePlugin(networkKey: string, gasOracleOptions?: gasOracleOptions) {
const gasStationApi = gasOracleOptions?.gasStationApi || POLYGON_GAS_STATION;
return new FetchUrlFeeDataNetworkPlugin(gasStationApi, async (fetchFeeData, provider, request) => {
if (!oracleMapper.has(networkKey)) {
oracleMapper.set(
networkKey,
GasPriceOracle__factory.connect(gasOracleOptions?.gasPriceOracle as string, provider),
);
}
if (!multicallMapper.has(networkKey)) {
multicallMapper.set(networkKey, Multicall__factory.connect(multiCallAddress, provider));
}
const Oracle = oracleMapper.get(networkKey) as GasPriceOracle;
const Multicall = multicallMapper.get(networkKey) as Multicall;
const [timestamp, heartbeat, feePerGas, priorityFeePerGas] = await multicall(Multicall, [
{
contract: Oracle,
name: 'timestamp',
},
{
contract: Oracle,
name: 'heartbeat',
},
{
contract: Oracle,
name: 'maxFeePerGas',
},
{
contract: Oracle,
name: 'maxPriorityFeePerGas',
},
]);
const isOutdated = Number(timestamp) <= Date.now() / 1000 - Number(heartbeat);
if (!isOutdated) {
const maxPriorityFeePerGas = (priorityFeePerGas * BigInt(13)) / BigInt(10);
const maxFeePerGas = feePerGas * BigInt(2) + maxPriorityFeePerGas;
return {
gasPrice: maxFeePerGas,
maxFeePerGas,
maxPriorityFeePerGas,
};
}
if (isNode) {
// Prevent Cloudflare from blocking our request in node.js
request.setHeader('User-Agent', 'ethers');
}
const [
{
bodyJson: { fast },
},
{ gasPrice },
] = await Promise.all([request.send(), fetchFeeData()]);
return {
gasPrice,
maxFeePerGas: parseUnits(`${fast.maxFee}`, 9),
maxPriorityFeePerGas: parseUnits(`${fast.maxPriorityFee}`, 9),
};
});
}
export function getProvider(netId: ChainIdType, rpcUrl: string, config: networkConfig): JsonRpcProvider {
const { networkName, gasPriceOracleContract, gasStationApi, pollInterval } = config;
const hasEns = ENS_CHAINS.includes(netId);
const staticNetwork = new Network(networkName, netId);
if (hasEns) {
staticNetwork.attachPlugin(new EnsPlugin(null, Number(netId)));
}
staticNetwork.attachPlugin(new GasCostPlugin());
if (gasPriceOracleContract) {
staticNetwork.attachPlugin(
getGasOraclePlugin(`${netId}_${rpcUrl}`, {
gasPriceOracle: gasPriceOracleContract,
gasStationApi,
}),
);
}
const provider = new JsonRpcProvider(rpcUrl, staticNetwork, {
staticNetwork,
});
provider.pollingInterval = pollInterval * 1000;
return provider;
}

View File

@ -1,107 +1,54 @@
import { MultiCall } from './contracts/MulticallAbi';
import { ITornadoPriceOracle, Token, TokenPrices, TokenSymbol } from './types';
import { MulticallAbi, OffchainOracleAbi } from './contracts';
import { getMultiCallContract, getOffchainOracleContract } from './contracts/factories';
import { Provider } from '@ethersproject/abstract-provider';
import { ethers, BigNumber } from 'ethers';
import { defaultAbiCoder } from 'ethers/lib/utils';
import { instances, Instances } from '@tornado/tornado-config';
import { ChainId } from './config';
import { Provider, parseEther } from 'ethers';
import { Multicall, Multicall__factory, OffchainOracle, OffchainOracle__factory } from './contracts';
import { ChainIdType, multiCallAddress } from './constants';
import { multicall } from './multicall';
import { getProvider } from './providers';
import { Token, TokenPrices, networkConfig } from './types';
/**
* Filter non-native tokens from Tornado instances config
* @param instances Tornado instances from torn-token config ('@tornado/tornado-config' library)
* @returns Array of non-native tokens
*/
function filterTokensFromTornadoInstances(instances: Instances): Token[] {
const ethInstances = Object.values(instances[ChainId.MAINNET]);
return ethInstances.filter((tokenInstance) => !!tokenInstance.tokenAddress) as Token[];
}
export class TokenPriceOracle {
oracle?: OffchainOracle;
multicall: Multicall;
provider: Provider;
const tornToken: Token = {
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
symbol: 'torn',
decimals: 18,
};
const tornadoTokens = [tornToken, ...filterTokensFromTornadoInstances(instances)];
constructor(netId: ChainIdType, rpcUrl: string, config: networkConfig) {
const { offchainOracleContract } = config;
const defaultTornadoTokenPrices: TokenPrices = {
torn: '1689423546359032',
dai: '598416104472725',
cdai: '13384388487019',
usdc: '599013776676721',
usdt: '599323410893614',
wbtc: '15659889148334216720',
};
this.provider = getProvider(netId, rpcUrl, config);
this.multicall = Multicall__factory.connect(multiCallAddress, this.provider);
export class TokenPriceOracle implements ITornadoPriceOracle {
private oracle: OffchainOracleAbi;
private multiCall: MulticallAbi;
private provider: Provider;
/**
* Constructs TokenPriceOracle class instance
* @param {string} rpcUrl http RPC (Ethereum Mainnet) url to fetch token prices from contract
* @param {Token[]} [tokens] Array of tokens
* @param {TokenPrices} [defaultTokenPrices] Default token prices, fallback if nothing loaded from contract
*/
constructor(
rpcUrl: string,
private tokens: Token[] = tornadoTokens,
private defaultTokenPrices: TokenPrices = defaultTornadoTokenPrices,
) {
this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
this.oracle = getOffchainOracleContract(this.provider);
this.multiCall = getMultiCallContract(this.provider);
}
// Instant return default token prices
get defaultPrices(): TokenPrices {
return this.defaultTokenPrices;
}
/**
* Prepare data for MultiCall contract
* @param {Token[]} [tokens] Tokens array
* @returns Valid structure to provide to MultiCall contract
*/
private prepareCallData(tokens: Token[] = this.tokens): MultiCall.CallStruct[] {
return tokens.map((token) => ({
to: this.oracle.address,
data: this.oracle.interface.encodeFunctionData('getRateToEth', [token.tokenAddress, true]),
}));
}
/**
* Fetch actual tokens price rate to ETH from offchain oracles
* @param {Token[]} [tokens] Token array
* @returns {TokenPrices} Object with token price rate to ETH in WEI
*/
async fetchPrices(tokens: Token[] = this.tokens): Promise<TokenPrices> {
try {
if (!tokens?.length) return {};
const callData = this.prepareCallData(tokens);
const { results, success } = await this.multiCall.multicall(callData);
const prices: TokenPrices = {};
for (let i = 0; i < results.length; i++) {
const tokenSymbol = tokens[i].symbol.toLowerCase() as TokenSymbol;
if (!success[i]) {
if (this.defaultTokenPrices[tokenSymbol]) prices[tokenSymbol] = this.defaultTokenPrices[tokenSymbol];
continue;
}
const decodedRate = defaultAbiCoder.decode(['uint256'], results[i]).toString();
const tokenDecimals = BigNumber.from(10).pow(tokens[i].decimals);
const ethDecimals = BigNumber.from(10).pow(18);
const price = BigNumber.from(decodedRate).mul(tokenDecimals).div(ethDecimals);
prices[tokenSymbol] = price.toString();
}
return prices;
} catch (e) {
console.error('Cannot get token prices, return default: ' + e);
return this.defaultTokenPrices;
if (offchainOracleContract) {
this.oracle = OffchainOracle__factory.connect(offchainOracleContract, this.provider);
}
}
async fetchPrices(tokens: Token[]): Promise<TokenPrices> {
// setup mock price for testnets
if (!this.oracle) {
return new Promise((resolve) =>
resolve(
tokens.reduce((acc, _, index) => {
acc[tokens[index].symbol] = parseEther('0.0001').toString();
return acc;
}, {} as TokenPrices),
),
);
}
const prices = (await multicall(
this.multicall,
tokens.map(({ tokenAddress }) => ({
contract: this.oracle,
name: 'getRateToEth',
params: [tokenAddress, true],
})),
)) as bigint[];
return prices.reduce((acc, price, index) => {
const tokenPriceInwei = (price * BigInt(10 ** tokens[index].decimals)) / BigInt(10 ** 18);
acc[tokens[index].symbol] = tokenPriceInwei.toString();
return acc;
}, {} as TokenPrices);
}
}

View File

@ -1,114 +1,31 @@
import { BigNumberish, BytesLike } from 'ethers';
import { GasPriceKey, GetTxGasParamsRes } from '@tornado/gas-price-oracle/lib/services';
import { AvailableTokenSymbols } from '@tornado/tornado-config';
import { InstanceTokenSymbol } from './config';
import type { BaseContract, Interface } from 'ethers';
// Type for big hexadecimal numbers, like 0x1eff87f47e37a0
export type HexadecimalStringifiedNumber = string;
export type LegacyGasPriceKey = GasPriceKey;
export type GasPriceParams = GetTxGasParamsRes;
export type LegacyGasPrices = {
[gasPriceType in LegacyGasPriceKey]: number;
};
/* Tornado-specific transaction types:
- 'user_withdrawal' - Fee calculation on user side, when user withdraw funds itself or via relayer on site
- 'relayer_withdrawal' - Fee calculation on relayer side, when relayer sends withdrawal transaction
- 'relayer_withdrawal_check_v4' - Fee calculation on relayer side, when V4 relayer checks user-provided fee. For compatibility reasons
- 'other' - Any other non-specific transaction
*/
export type WithdrawalTxType = 'user_withdrawal' | 'relayer_withdrawal' | 'relayer_withdrawal_check_v4';
export type TxType = WithdrawalTxType | 'other';
export interface TransactionData {
to: string;
from?: string;
nonce?: number;
gasLimit?: BigNumberish;
gasPrice?: BigNumberish;
data?: string;
value: BigNumberish;
chainId?: number;
type?: number;
maxFeePerGas?: BigNumberish;
maxPriorityFeePerGas?: BigNumberish;
}
export interface ITornadoFeeOracle {
getGasParams: (params?: GetGasParamsInput) => Promise<GetGasParamsRes>;
getGas: (params?: GetGasInput) => Promise<HexadecimalStringifiedNumber>;
getGasPriceParams: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<GasPriceParams>;
getGasPrice: (speed?: LegacyGasPriceKey, bumpPercent?: number) => Promise<HexadecimalStringifiedNumber>;
getGasLimit: (tx?: TransactionData, type?: TxType, bumpPercent?: number) => Promise<number>;
fetchL1OptimismFee: (tx?: TransactionData) => Promise<HexadecimalStringifiedNumber>;
calculateRefundInETH: (gasPrice: BigNumberish, tokenSymbol: InstanceTokenSymbol) => HexadecimalStringifiedNumber;
fetchRefundInETH: (tokenSymbol: InstanceTokenSymbol) => Promise<HexadecimalStringifiedNumber>;
calculateRefundInToken: (
gasPrice: BigNumberish,
tokenPriceInEth: BigNumberish,
tokenDecimals: HexadecimalStringifiedNumber | number,
tokenSymbol: InstanceTokenSymbol,
) => HexadecimalStringifiedNumber;
calculateWithdrawalFeeViaRelayer: (params: GetWithdrawalFeeViaRelayerInput) => Promise<HexadecimalStringifiedNumber>;
}
export interface ITornadoPriceOracle {
defaultPrices: TokenPrices;
fetchPrices: (tokens?: Token[]) => Promise<TokenPrices>;
}
export type WithdrawalData = {
contract: string;
proof: BytesLike;
args: [BytesLike, BytesLike, string, string, BigNumberish, BigNumberish];
};
export type TornadoPoolInstance = {
currency: AvailableTokenSymbols;
amount: number;
decimals: number;
};
// All non-native tokens from Tornado instances on ETH mainnet and TORN itself
export type TokenSymbol = 'dai' | 'cdai' | 'usdc' | 'usdt' | 'wbtc' | 'torn';
export type Token = {
tokenAddress: string;
symbol: TokenSymbol;
symbol: string;
decimals: number;
};
export type TokenPrices = { [tokenSymbol in TokenSymbol]?: BigNumberish };
export type TokenPrices = { [tokenSymbol in string]?: string };
// Reponse type for getGasParams function of fee oracle
export type GetGasParamsRes = {
gasLimit: number;
gasPrice: HexadecimalStringifiedNumber; // Gas price in native currency
};
export interface Call3 {
contract?: BaseContract;
address?: string;
interface?: Interface;
name: string;
params?: any[];
allowFailure?: boolean;
}
export type GetGasInput = {
// Transaction type: user-side calculation, relayer-side calculation or
// relayer calculation to check user-provided fee in old V4 relayer (for backwards compatibility)
txType?: TxType;
tx?: TransactionData; // Transaction data in ethers format
predefinedGasPrice?: HexadecimalStringifiedNumber; // Predefined gas price for withdrawal tx (wont be calculated again in function)
predefinedGasLimit?: number; // Predefined gas limit for withdrawal tx (wont be calculated again in function)
bumpGasLimitPercent?: number; // Gas limit bump percent to prioritize transaction (recenlty used)
bumpGasPricePercent?: number; // Gas price bump percent to prioritize transaction (rarely used)
speed?: LegacyGasPriceKey; // Preferred transaction speed, if uses legacy gas (before EIP-1559)
};
export interface gasOracleOptions {
gasPriceOracle?: string;
gasStationApi?: string;
}
export type GetGasParamsInput = GetGasInput & { includeL1FeeToGasLimit?: boolean };
export type GetWithdrawalFeeViaRelayerInput = {
// Transaction type: user-side calculation, relayer-side calculation or
// relayer calculation to check user-provided fee in old V4 relayer (for backwards compatibility)
txType: WithdrawalTxType;
tx?: TransactionData; // Transaction data in ethers format
relayerFeePercent: number | string; // Relayer fee percent from withdrawal amount (for example, 0.15 for BNB or 0.4 for ETH Mainnet)
currency: AvailableTokenSymbols | Uppercase<AvailableTokenSymbols>; // Currency (token) symbol
amount: string | number; // Withdrawal amount in selected currency (10 for 10 ETH, 10000 for 10000 DAI)
decimals: string | number; // Token (currency) decimal places
refundInEth?: HexadecimalStringifiedNumber; // Refund amount in ETH, if withdrawing non-native currency on ETH Mainnet or Goerli
tokenPriceInEth?: HexadecimalStringifiedNumber | string; // Token (currency) price in ETH wei, if withdrawing non-native currency
predefinedGasPrice?: HexadecimalStringifiedNumber; // Predefined gas price for withdrawal tx (wont be calculated again in function)
predefinedGasLimit?: number; // Predefined gas limit for withdrawal tx (wont be calculated again in function)
};
export interface networkConfig {
networkName: string;
offchainOracleContract?: string;
optimismL1FeeOracleAddress?: string;
gasPriceOracleContract?: string;
gasStationApi?: string;
pollInterval: number;
}

View File

@ -1,44 +1,30 @@
import { serialize } from '@ethersproject/transactions';
import { GasPriceParams, TransactionData } from './types';
import { BigNumber, BigNumberish } from 'ethers';
import BigNumberFloat from 'bignumber.js';
BigNumberFloat.config({ EXPONENTIAL_AT: 100 });
import type { BigNumberish } from 'ethers';
const GWEI = 1e9;
export const isNode =
!(
(
process as typeof process & {
browser?: boolean;
}
).browser
// prettier-ignore
// @ts-ignore
) && typeof globalThis.window === 'undefined';
export function serializeTx(tx?: TransactionData | string): string {
if (!tx) tx = '0x';
if (typeof tx === 'string') return tx;
return serialize(tx);
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function calculateGasPriceInWei(gasPrice: GasPriceParams): BigNumber {
// @ts-ignore
return BigNumber.from(gasPrice.gasPrice || gasPrice.maxFeePerGas);
}
export function bump(value: BigNumberish, percent: number): BigNumber {
const hundredPercents = BigNumberFloat(100);
return BigNumber.from(
BigNumberFloat(BigNumber.from(value).toHexString())
.times(hundredPercents.plus(BigNumberFloat(percent)))
.div(hundredPercents)
.decimalPlaces(0, 1)
.toString(),
);
}
export function fromGweiToWeiHex(value: number | string): BigNumberish {
return BigNumber.from(BigNumberFloat(value).times(GWEI).toString()).toHexString();
}
export function convertETHToToken(
amountInWEI: BigNumberish,
tokenDecimals: number | string,
/**
* Example:
*
* amountInWei (0.1 ETH) * tokenDecimals (18) * tokenPriceInWei (0.0008) = 125 TOKEN
*/
export function convertETHToTokenAmount(
amountInWei: BigNumberish,
tokenPriceInWei: BigNumberish,
): BigNumber {
const tokenDecimalsMultiplier = BigNumber.from(10).pow(tokenDecimals);
return BigNumber.from(amountInWEI).mul(tokenDecimalsMultiplier).div(tokenPriceInWei);
tokenDecimals: number = 18,
): bigint {
const tokenDecimalsMultiplier = BigInt(10 ** Number(tokenDecimals));
return (BigInt(amountInWei) * tokenDecimalsMultiplier) / BigInt(tokenPriceInWei);
}