From 4b3970732137cb58b4220ea6e2364071baa8dd8c Mon Sep 17 00:00:00 2001 From: Leonid Tyurin Date: Fri, 14 Jul 2023 17:34:53 +0400 Subject: [PATCH] Fee manager improvements (#187) --- CONFIGURATION.md | 6 + zp-relayer/configs/relayerConfig.ts | 9 + zp-relayer/endpoints.ts | 2 +- zp-relayer/init.ts | 2 +- zp-relayer/pool.ts | 4 +- zp-relayer/queue/poolTxQueue.ts | 2 +- zp-relayer/services/fee/DynamicFeeManager.ts | 30 ++-- zp-relayer/services/fee/FeeManager.ts | 161 +++++++++++++----- zp-relayer/services/fee/OptimismFeeManager.ts | 74 ++------ zp-relayer/services/fee/StaticFeeManager.ts | 10 +- zp-relayer/services/price-feed/IPriceFeed.ts | 4 +- .../services/price-feed/NativePriceFeed.ts | 11 +- .../services/price-feed/OneInchPriceFeed.ts | 32 ++-- zp-relayer/state/PoolState.ts | 6 +- zp-relayer/utils/constants.ts | 2 + zp-relayer/utils/helpers.ts | 6 + zp-relayer/validation/tx/validateTx.ts | 26 ++- zp-relayer/workers/poolTxWorker.ts | 10 +- zp-relayer/workers/workerTypes.ts | 3 +- 19 files changed, 239 insertions(+), 161 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index fd15aa3d..2dd1a591 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -59,6 +59,12 @@ These environment variables are required for all services. | RELAYER_PRICE_FEED_TYPE | Price feed type that will be used for rate conversions. | PriceFeedType | | RELAYER_PRICE_FEED_CONTRACT_ADDRESS | Price feed contract address. | address | | RELAYER_PRICE_FEED_BASE_TOKEN_ADDRESS | Base token that will be used for rate conversions. | address | +| RELAYER_MIN_BASE_FEE | Min base fee for each tx type for `dynamic` and `optimism` FeeManagers. Does not affect any extra fee parameters such as per byte fee or native swap fee. Defaults to `0`. | integer | +| RELAYER_BASE_TX_GAS_DEPOSIT | Base gas consumption for deposit transaction without variable per byte memo fee or any other features such as native swap fee. Defaults to `650000`. | integer | +| RELAYER_BASE_TX_GAS_PERMITTABLE_DEPOSIT | Same as `RELAYER_BASE_TX_GAS_DEPOSIT`, but for permittable deposits. Defaults to `650000`. | integer | +| RELAYER_BASE_TX_GAS_TRANSFER | Same as `RELAYER_BASE_TX_GAS_DEPOSIT`, but for transfers. Defaults to `650000`. | integer | +| RELAYER_BASE_TX_GAS_WITHDRAWAL | Same as `RELAYER_BASE_TX_GAS_DEPOSIT`, but for withdrawals. Defaults to `650000`. | integer | +| RELAYER_BASE_TX_GAS_NATIVE_CONVERT | Gas consumption for swapping pool's token to native token during withdrawal. Defaults to `200000`. | integer | ## Watcher diff --git a/zp-relayer/configs/relayerConfig.ts b/zp-relayer/configs/relayerConfig.ts index afbe63ba..e3a56575 100644 --- a/zp-relayer/configs/relayerConfig.ts +++ b/zp-relayer/configs/relayerConfig.ts @@ -8,6 +8,7 @@ import { ProverType } from '@/prover' import { countryCodes } from '@/utils/countryCodes' import { logger } from '@/services/appLogger' import { PermitType } from '@/utils/permit/types' +import { TxType } from 'zp-memo-parser' const relayerAddress = new Web3().eth.accounts.privateKeyToAccount( process.env.RELAYER_ADDRESS_PRIVATE_KEY as string @@ -25,6 +26,7 @@ const config = { relayerPrivateKey: process.env.RELAYER_ADDRESS_PRIVATE_KEY as string, tokenAddress: process.env.RELAYER_TOKEN_ADDRESS as string, relayerGasLimit: toBN(process.env.RELAYER_GAS_LIMIT as string), + minBaseFee: toBN(process.env.RELAYER_MIN_BASE_FEE || '0'), relayerFee: process.env.RELAYER_FEE ? toBN(process.env.RELAYER_FEE) : null, maxNativeAmount: toBN(process.env.RELAYER_MAX_NATIVE_AMOUNT || '0'), treeUpdateParamsPath: process.env.RELAYER_TREE_UPDATE_PARAMS_PATH || './params/tree_params.bin', @@ -74,6 +76,13 @@ const config = { priceFeedBaseTokenAddress: process.env.RELAYER_PRICE_FEED_BASE_TOKEN_ADDRESS || null, precomputeParams: process.env.RELAYER_PRECOMPUTE_PARAMS === 'true', permitType: (process.env.RELAYER_PERMIT_TYPE || PermitType.SaltedPermit) as PermitType, + baseTxGas: { + [TxType.DEPOSIT]: toBN(process.env.RELAYER_BASE_TX_GAS_DEPOSIT || '650000'), + [TxType.PERMITTABLE_DEPOSIT]: toBN(process.env.RELAYER_BASE_TX_GAS_PERMITTABLE_DEPOSIT || '650000'), + [TxType.TRANSFER]: toBN(process.env.RELAYER_BASE_TX_GAS_TRANSFER || '650000'), + [TxType.WITHDRAWAL]: toBN(process.env.RELAYER_BASE_TX_GAS_WITHDRAWAL || '650000'), + nativeConvertOverhead: toBN(process.env.RELAYER_BASE_TX_GAS_NATIVE_CONVERT || '200000'), + }, } export default config diff --git a/zp-relayer/endpoints.ts b/zp-relayer/endpoints.ts index a6a95141..5fec68bc 100644 --- a/zp-relayer/endpoints.ts +++ b/zp-relayer/endpoints.ts @@ -210,7 +210,7 @@ function getFeeBuilder(feeManager: FeeManager) { return async (req: Request, res: Response) => { validateBatch([[checkTraceId, req.headers]]) - const feeOptions = await feeManager.getFeeOptions({ gasLimit: config.relayerGasLimit }) + const feeOptions = await feeManager.getFeeOptions() const fees = feeOptions.denominate(pool.denominator).getObject() res.json(fees) diff --git a/zp-relayer/init.ts b/zp-relayer/init.ts index 68a85af7..4513cff1 100644 --- a/zp-relayer/init.ts +++ b/zp-relayer/init.ts @@ -41,7 +41,6 @@ function buildFeeManager( scaleFactor: config.feeScalingFactor, marginFactor: config.feeMarginFactor, updateInterval: config.feeManagerUpdateInterval, - defaultFeeOptionsParams: { gasLimit: config.relayerGasLimit }, } if (type === FeeManagerType.Static) { if (config.relayerFee === null) throw new Error('Static relayer fee is not set') @@ -104,6 +103,7 @@ export async function init() { ) const priceFeed = buildPriceFeed(config.priceFeedType, web3) + await priceFeed.init() const feeManager = buildFeeManager(config.feeManagerType, priceFeed, gasPriceService, web3) await feeManager.start() diff --git a/zp-relayer/pool.ts b/zp-relayer/pool.ts index 17272466..314d17de 100644 --- a/zp-relayer/pool.ts +++ b/zp-relayer/pool.ts @@ -284,7 +284,7 @@ class Pool { tier: toBN(limits.tier), dailyUserDirectDepositCap: toBN(limits.dailyUserDirectDepositCap), dailyUserDirectDepositCapUsage: toBN(limits.dailyUserDirectDepositCapUsage), - directDepositCap: toBN(limits.directDepositCap) + directDepositCap: toBN(limits.directDepositCap), } } @@ -316,7 +316,7 @@ class Pool { dailyForAddress: { total: limits.dailyUserDirectDepositCap.toString(10), available: limits.dailyUserDirectDepositCap.sub(limits.dailyUserDirectDepositCapUsage).toString(10), - } + }, }, tier: limits.tier.toString(10), } diff --git a/zp-relayer/queue/poolTxQueue.ts b/zp-relayer/queue/poolTxQueue.ts index 0bcf3f3a..9f64ce82 100644 --- a/zp-relayer/queue/poolTxQueue.ts +++ b/zp-relayer/queue/poolTxQueue.ts @@ -1,7 +1,7 @@ import { Queue } from 'bullmq' import { TX_QUEUE_NAME } from '@/utils/constants' import type { Proof } from 'libzkbob-rs-node' -import { TxType } from 'zp-memo-parser' +import type { TxType } from 'zp-memo-parser' import { redis } from '@/services/redisClient' export interface TxPayload { diff --git a/zp-relayer/services/fee/DynamicFeeManager.ts b/zp-relayer/services/fee/DynamicFeeManager.ts index 3270bd79..6bb1568f 100644 --- a/zp-relayer/services/fee/DynamicFeeManager.ts +++ b/zp-relayer/services/fee/DynamicFeeManager.ts @@ -1,12 +1,7 @@ import { toBN } from 'web3-utils' -import { - FeeManager, - FeeEstimate, - DefaultUserFeeOptions, - IFeeEstimateParams, - IGetFeesParams, - IFeeManagerConfig, -} from './FeeManager' +import { FeeManager, FeeEstimate, IFeeEstimateParams, IFeeManagerConfig, DynamicFeeOptions } from './FeeManager' +import { NZERO_BYTE_GAS } from '@/utils/constants' +import relayerConfig from '@/configs/relayerConfig' import type { EstimationType, GasPrice } from '../gas-price' export class DynamicFeeManager extends FeeManager { @@ -16,13 +11,20 @@ export class DynamicFeeManager extends FeeManager { async init() {} - async _estimateFee(_params: IFeeEstimateParams, feeOptions: DefaultUserFeeOptions) { - const fee = feeOptions.getObject().fee - return new FeeEstimate(toBN(fee)) + async _estimateFee({ txType, nativeConvert, txData }: IFeeEstimateParams, feeOptions: DynamicFeeOptions) { + const { [txType]: baseFee, nativeConvertFee, oneByteFee } = feeOptions.fees + // -1 to account for the 0x prefix + const calldataLen = (txData.length >> 1) - 1 + const fee = baseFee.add(oneByteFee.muln(calldataLen)) + if (nativeConvert) { + fee.iadd(nativeConvertFee) + } + return new FeeEstimate({ fee }) } - async _fetchFeeOptions({ gasLimit }: IGetFeesParams) { - const baseFee = await FeeManager.estimateExecutionFee(this.gasPrice, gasLimit) - return new DefaultUserFeeOptions(baseFee) + async _fetchFeeOptions(): Promise { + const gasPrice = await this.gasPrice.fetchOnce() + const oneByteFee = FeeManager.executionFee(gasPrice, toBN(NZERO_BYTE_GAS)) + return DynamicFeeOptions.fromGasPice(gasPrice, oneByteFee, relayerConfig.minBaseFee) } } diff --git a/zp-relayer/services/fee/FeeManager.ts b/zp-relayer/services/fee/FeeManager.ts index 8a404c68..8200f937 100644 --- a/zp-relayer/services/fee/FeeManager.ts +++ b/zp-relayer/services/fee/FeeManager.ts @@ -1,58 +1,139 @@ -import type BN from 'bn.js' +import BN from 'bn.js' import { toBN } from 'web3-utils' import type { IPriceFeed } from '../price-feed/IPriceFeed' -import { GasPrice, EstimationType, getMaxRequiredGasPrice } from '../gas-price' -import { setIntervalAndRun } from '@/utils/helpers' +import { getMaxRequiredGasPrice, GasPriceValue } from '../gas-price' +import { applyDenominator, setIntervalAndRun } from '@/utils/helpers' import { logger } from '../appLogger' +import { TxType } from 'zp-memo-parser' +import config from '@/configs/relayerConfig' -export interface IGetFeesParams { - gasLimit: BN +export interface IFeeEstimateParams { + txType: TxType + nativeConvert: boolean + txData: string } -export interface IFeeEstimateParams extends IGetFeesParams { - extraData: string + +interface NestedRecord { + [key: string]: T | NestedRecord } -export interface IUserFeeOptions { +type Fees = { [k in K[number]]: V } +export interface IFeeOptions { + fees: Fees applyFactor(factor: BN): this + applyMinBound(): this denominate(denominator: BN): this convert(priceFeed: IPriceFeed): Promise - getObject(): Record + getObject(): NestedRecord clone(): this } -export class DefaultUserFeeOptions implements IUserFeeOptions { - constructor(protected fee: BN) {} +export class FeeOptions implements IFeeOptions { + constructor(public fees: Fees, private readonly minFees?: Fees) {} + + private mapI(f: (v: BN, k: T[number]) => BN) { + let k: T[number] + for (k in this.fees) { + this.fees[k] = f(this.fees[k], k) + } + } + + private mapClone(f: (v: BN, k: T[number]) => V) { + const clone = {} as Fees + let k: T[number] + for (k in this.fees) { + clone[k] = f(this.fees[k], k) + } + return clone + } applyFactor(factor: BN) { - this.fee = this.fee.mul(factor).divn(100) + this.mapI(p => p.mul(factor).divn(100)) return this } denominate(denominator: BN): this { - this.fee = this.fee.div(denominator) + const dInverse = toBN(1).shln(255) + this.mapI(p => applyDenominator(p, denominator.xor(dInverse))) return this } async convert(priceFeed: IPriceFeed) { - const [fee] = await priceFeed.convert([this.fee]) - this.fee = fee + const rate = await priceFeed.getRate() + this.mapI(p => priceFeed.convert(rate, p)) + return this + } + + applyMinBound() { + const minFees = this.minFees + if (!minFees) { + return this + } + this.mapI((p, k) => BN.max(p, minFees[k])) return this } clone() { - return new DefaultUserFeeOptions(this.fee.clone()) as this + const cloneBN = (p: BN) => p.clone() + // A little hack to not override `clone` for subtypes + // NOTE: requires all subtypes to have the same constructor signature + return new (this.constructor as typeof FeeOptions)(this.mapClone(cloneBN), this.minFees) as this } - getObject() { + getObject(): NestedRecord { + return this.mapClone(p => p.toString(10)) + } +} + +type DynamicFeeKeys = [ + TxType.DEPOSIT, + TxType.PERMITTABLE_DEPOSIT, + TxType.TRANSFER, + TxType.WITHDRAWAL, + 'oneByteFee', + 'nativeConvertFee' +] +// Utility class for dynamic fee estimations +export class DynamicFeeOptions extends FeeOptions { + static fromGasPice(gasPrice: GasPriceValue, oneByteFee: BN, minFee: BN) { + const getFee = (txType: TxType) => FeeManager.executionFee(gasPrice, config.baseTxGas[txType]) + const fees: Fees = { + [TxType.DEPOSIT]: getFee(TxType.DEPOSIT), + [TxType.PERMITTABLE_DEPOSIT]: getFee(TxType.PERMITTABLE_DEPOSIT), + [TxType.TRANSFER]: getFee(TxType.TRANSFER), + [TxType.WITHDRAWAL]: getFee(TxType.WITHDRAWAL), + oneByteFee, + nativeConvertFee: FeeManager.executionFee(gasPrice, config.baseTxGas.nativeConvertOverhead), + } + const minFees: Fees = { + [TxType.DEPOSIT]: minFee, + [TxType.PERMITTABLE_DEPOSIT]: minFee, + [TxType.TRANSFER]: minFee, + [TxType.WITHDRAWAL]: minFee, + oneByteFee: toBN(0), + nativeConvertFee: toBN(0), + } + return new DynamicFeeOptions(fees, minFees) + } + + override getObject() { return { - fee: this.fee.toString(10), + fee: { + deposit: this.fees[TxType.DEPOSIT].toString(10), + transfer: this.fees[TxType.TRANSFER].toString(10), + withdrawal: this.fees[TxType.WITHDRAWAL].toString(10), + permittableDeposit: this.fees[TxType.PERMITTABLE_DEPOSIT].toString(10), + }, + oneByteFee: this.fees.oneByteFee.toString(10), + nativeConvertFee: this.fees.nativeConvertFee.toString(10), } } } -export class FeeEstimate extends DefaultUserFeeOptions { +// Utility class for internal fee estimations +export class FeeEstimate extends FeeOptions<['fee']> { getEstimate() { - return this.fee + return this.fees.fee } } @@ -61,11 +142,10 @@ export interface IFeeManagerConfig { scaleFactor: BN marginFactor: BN updateInterval: number - defaultFeeOptionsParams: IGetFeesParams } -export abstract class FeeManager { - private cachedFeeOptions: IUserFeeOptions | null = null +export abstract class FeeManager { + private cachedFeeOptions: IFeeOptions | null = null private updateFeeOptionsInterval: NodeJS.Timeout | null = null constructor(protected config: IFeeManagerConfig) {} @@ -78,7 +158,7 @@ export abstract class FeeManager { if (this.updateFeeOptionsInterval) clearInterval(this.updateFeeOptionsInterval) this.updateFeeOptionsInterval = await setIntervalAndRun(async () => { - const feeOptions = await this.fetchFeeOptions(this.config.defaultFeeOptionsParams) + const feeOptions = await this.fetchFeeOptions() logger.debug('Updating cached fee options', { old: this.cachedFeeOptions?.getObject(), new: feeOptions.getObject(), @@ -87,36 +167,30 @@ export abstract class FeeManager { }, this.config.updateInterval) } - static async estimateExecutionFee(gasPrice: GasPrice, gasLimit: BN): Promise { - const price = await gasPrice.fetchOnce() - return toBN(getMaxRequiredGasPrice(price)).mul(gasLimit) - } - - private async convertAndScale(baseFee: T) { - const fees = await baseFee.convert(this.config.priceFeed) - const scaledFees = fees.applyFactor(this.config.scaleFactor) - return scaledFees + static executionFee(gasPrice: GasPriceValue, gasLimit: BN): BN { + return toBN(getMaxRequiredGasPrice(gasPrice)).mul(gasLimit) } async estimateFee(params: IFeeEstimateParams): Promise { - const fees = await this.getFeeOptions(params, false) + const fees = await this.getFeeOptions(false) const estimatedFee = await this._estimateFee(params, fees) const marginedFee = estimatedFee.applyFactor(this.config.marginFactor) return marginedFee } - async fetchFeeOptions(params: IGetFeesParams): Promise { - const feeOptions = await this._fetchFeeOptions(params) - const convertedFees = await this.convertAndScale(feeOptions) + async fetchFeeOptions(): Promise> { + const feeOptions = await this._fetchFeeOptions() + const convertedFees = await feeOptions.convert(this.config.priceFeed) + const scaledFees = convertedFees.applyFactor(this.config.scaleFactor) - return convertedFees + return scaledFees } - async getFeeOptions(params: IGetFeesParams, useCached = true): Promise { + async getFeeOptions(useCached = true): Promise> { if (useCached && this.cachedFeeOptions) return this.cachedFeeOptions.clone() - let feeOptions: IUserFeeOptions + let feeOptions: IFeeOptions try { - feeOptions = await this.fetchFeeOptions(params) + feeOptions = await this.fetchFeeOptions() logger.debug('Fetched fee options', feeOptions.getObject()) } catch (e) { logger.error('Failed to fetch fee options', e) @@ -124,12 +198,13 @@ export abstract class FeeManager { logger.debug('Fallback to cache fee options') feeOptions = this.cachedFeeOptions.clone() } + feeOptions.applyMinBound() return feeOptions } // Should be used for tx fee validation - protected abstract _estimateFee(params: IFeeEstimateParams, fees: IUserFeeOptions): Promise + protected abstract _estimateFee(params: IFeeEstimateParams, fees: IFeeOptions): Promise // Should provide fee estimations for users - protected abstract _fetchFeeOptions(params: IGetFeesParams): Promise + protected abstract _fetchFeeOptions(): Promise> } diff --git a/zp-relayer/services/fee/OptimismFeeManager.ts b/zp-relayer/services/fee/OptimismFeeManager.ts index aad894d3..f1e8cf4e 100644 --- a/zp-relayer/services/fee/OptimismFeeManager.ts +++ b/zp-relayer/services/fee/OptimismFeeManager.ts @@ -1,58 +1,15 @@ import type Web3 from 'web3' import type BN from 'bn.js' import type { Contract } from 'web3-eth-contract' -import { OP_GAS_ORACLE_ADDRESS, MOCK_CALLDATA } from '@/utils/constants' +import { OP_GAS_ORACLE_ADDRESS } from '@/utils/constants' import { AbiItem, toBN, hexToBytes } from 'web3-utils' import OracleAbi from '@/abi/op-oracle.json' import { contractCallRetry } from '@/utils/helpers' -import { - FeeManager, - FeeEstimate, - IFeeEstimateParams, - IFeeManagerConfig, - IUserFeeOptions, - IGetFeesParams, -} from './FeeManager' -import type { IPriceFeed } from '../price-feed' +import { FeeManager, FeeEstimate, IFeeEstimateParams, IFeeManagerConfig, DynamicFeeOptions } from './FeeManager' +import relayerConfig from '@/configs/relayerConfig' +import { ZERO_BYTE_GAS, NZERO_BYTE_GAS } from '@/utils/constants' import type { EstimationType, GasPrice } from '../gas-price' -const ZERO_BYTE_GAS = 4 -const NZERO_BYTE_GAS = 16 - -class OptimismUserFeeOptions implements IUserFeeOptions { - constructor(private baseFee: BN, private oneByteFee: BN) {} - - applyFactor(factor: BN) { - this.baseFee = this.baseFee.mul(factor).divn(100) - this.oneByteFee = this.oneByteFee.mul(factor).divn(100) - return this - } - - denominate(denominator: BN): this { - this.baseFee = this.baseFee.div(denominator) - this.oneByteFee = this.oneByteFee.div(denominator) - return this - } - - async convert(priceFeed: IPriceFeed) { - const [l2fee, oneByteFee] = await priceFeed.convert([this.baseFee, this.oneByteFee]) - this.baseFee = l2fee - this.oneByteFee = oneByteFee - return this - } - - clone(): this { - return new OptimismUserFeeOptions(this.baseFee.clone(), this.oneByteFee.clone()) as this - } - - getObject() { - return { - fee: this.baseFee.toString(10), - oneByteFee: this.oneByteFee.toString(10), - } - } -} - export class OptimismFeeManager extends FeeManager { private oracle: Contract private overhead!: BN @@ -87,28 +44,29 @@ export class OptimismFeeManager extends FeeManager { return scaled } - async _estimateFee({ extraData }: IFeeEstimateParams, feeOptions: OptimismUserFeeOptions) { - const { fee: baseFee, oneByteFee } = feeOptions.getObject() + async _estimateFee({ txType, nativeConvert, txData }: IFeeEstimateParams, feeOptions: DynamicFeeOptions) { + const { [txType]: baseFee, nativeConvertFee, oneByteFee } = feeOptions.fees - const unscaledL1Fee = this.getL1Fee(MOCK_CALLDATA + extraData, toBN(oneByteFee)) + const unscaledL1Fee = this.getL1Fee(txData, oneByteFee) // Because oneByteFee = l1BaseFee * NZERO_BYTE_GAS, we need to divide the estimation // We do it here to get a more accurate result const l1Fee = unscaledL1Fee.divn(NZERO_BYTE_GAS) - const feeEstimate = toBN(baseFee).add(l1Fee) - - return new FeeEstimate(feeEstimate) + const fee = baseFee.add(l1Fee) + if (nativeConvert) { + fee.iadd(nativeConvertFee) + } + return new FeeEstimate({ fee }) } - async _fetchFeeOptions({ gasLimit }: IGetFeesParams): Promise { - // TODO: add RLP encoding overhead to baseFee - const baseFee = await FeeManager.estimateExecutionFee(this.gasPrice, gasLimit) + async _fetchFeeOptions(): Promise { + const gasPrice = await this.gasPrice.fetchOnce() const l1BaseFee = await contractCallRetry(this.oracle, 'l1BaseFee').then(toBN) - // Use an upper bound for the oneByteFee + const oneByteFee = l1BaseFee.muln(NZERO_BYTE_GAS) - return new OptimismUserFeeOptions(baseFee, oneByteFee) + return DynamicFeeOptions.fromGasPice(gasPrice, oneByteFee, relayerConfig.minBaseFee) } } diff --git a/zp-relayer/services/fee/StaticFeeManager.ts b/zp-relayer/services/fee/StaticFeeManager.ts index c908eb93..19830dd7 100644 --- a/zp-relayer/services/fee/StaticFeeManager.ts +++ b/zp-relayer/services/fee/StaticFeeManager.ts @@ -1,5 +1,5 @@ import type BN from 'bn.js' -import { FeeManager, FeeEstimate, DefaultUserFeeOptions, IFeeManagerConfig } from './FeeManager' +import { FeeManager, FeeEstimate, IFeeManagerConfig, FeeOptions } from './FeeManager' export class StaticFeeManager extends FeeManager { constructor(config: IFeeManagerConfig, private readonly staticFee: BN) { @@ -9,10 +9,14 @@ export class StaticFeeManager extends FeeManager { async init() {} async _estimateFee() { - return new FeeEstimate(this.staticFee) + return new FeeEstimate({ + fee: this.staticFee, + }) } async _fetchFeeOptions() { - return new DefaultUserFeeOptions(this.staticFee) + return new FeeOptions({ + fee: this.staticFee, + }) } } diff --git a/zp-relayer/services/price-feed/IPriceFeed.ts b/zp-relayer/services/price-feed/IPriceFeed.ts index 2dfa0416..7ecaab37 100644 --- a/zp-relayer/services/price-feed/IPriceFeed.ts +++ b/zp-relayer/services/price-feed/IPriceFeed.ts @@ -1,5 +1,7 @@ import type BN from 'bn.js' export interface IPriceFeed { - convert(baseTokenAmounts: BN[]): Promise + init(): Promise + getRate(): Promise + convert(rate: BN, baseTokenAmounts: BN): BN } diff --git a/zp-relayer/services/price-feed/NativePriceFeed.ts b/zp-relayer/services/price-feed/NativePriceFeed.ts index dee726c5..fbff7345 100644 --- a/zp-relayer/services/price-feed/NativePriceFeed.ts +++ b/zp-relayer/services/price-feed/NativePriceFeed.ts @@ -1,10 +1,17 @@ import type BN from 'bn.js' import type { IPriceFeed } from './IPriceFeed' +import { toBN } from 'web3-utils' export class NativePriceFeed implements IPriceFeed { constructor() {} - async convert(baseTokenAmounts: BN[]): Promise { - return baseTokenAmounts + async init() {} + + async getRate(): Promise { + return toBN(1) + } + + convert(_: BN, baseTokenAmount: BN): BN { + return baseTokenAmount } } diff --git a/zp-relayer/services/price-feed/OneInchPriceFeed.ts b/zp-relayer/services/price-feed/OneInchPriceFeed.ts index dd62d7f9..a709c9b9 100644 --- a/zp-relayer/services/price-feed/OneInchPriceFeed.ts +++ b/zp-relayer/services/price-feed/OneInchPriceFeed.ts @@ -11,10 +11,8 @@ import OracleAbi from '@/abi/one-inch-oracle.json' export class OneInchPriceFeed implements IPriceFeed { private contract: Contract private baseTokenAddress: string - private poolTokenAddress: string - - tokenDecimals!: BN private baseTokenDecimals!: BN + private poolTokenAddress: string constructor( private web3: Web3, @@ -29,29 +27,27 @@ export class OneInchPriceFeed implements IPriceFeed { this.contract = new web3.eth.Contract(OracleAbi as AbiItem[], contractAddress) } + async init() { + if (this.baseTokenAddress !== ZERO_ADDRESS) { + this.baseTokenDecimals = await this.getContractDecimals(this.baseTokenAddress) + } else { + this.baseTokenDecimals = toBN(toWei('1')) // 1 ether + } + } + private async getContractDecimals(contractAddress: string): Promise { const contract = new this.web3.eth.Contract(Erc20Abi as AbiItem[], contractAddress) const decimals = await contract.methods.decimals().call() return toBN(10).pow(toBN(decimals)) } - async getBaseTokenDecimals(): Promise { - if (!this.baseTokenDecimals) { - this.baseTokenDecimals = - this.baseTokenAddress !== ZERO_ADDRESS - ? await this.getContractDecimals(this.baseTokenAddress) - : toBN(toWei('1')) // 1 ether - } - return this.baseTokenDecimals + getRate(): Promise { + return this.contract.methods.getRate(this.baseTokenAddress, this.poolTokenAddress, true).call().then(toBN) } - async convert(baseTokenAmounts: BN[]): Promise { - const baseDecimals = await this.getBaseTokenDecimals() - const rate = await this.contract.methods - .getRate(this.baseTokenAddress, this.poolTokenAddress, true) - .call() - .then(toBN) + convert(rate: BN, baseTokenAmount: BN): BN { + const baseDecimals = this.baseTokenDecimals - return baseTokenAmounts.map(a => a.mul(rate).div(baseDecimals)) + return baseTokenAmount.mul(rate).div(baseDecimals) } } diff --git a/zp-relayer/state/PoolState.ts b/zp-relayer/state/PoolState.ts index 5b025a55..3f73505c 100644 --- a/zp-relayer/state/PoolState.ts +++ b/zp-relayer/state/PoolState.ts @@ -145,8 +145,12 @@ export class PoolState { } async getTransactions(limit: number, offset: number) { + // Round offset to OUTPLUSONE + offset = Math.floor(offset / OUTPLUSONE) * OUTPLUSONE + const txs: string[] = [] - let nextOffset = Math.floor(offset / OUTPLUSONE) * OUTPLUSONE + + let nextOffset = offset for (let i = 0; i < limit; i++) { nextOffset = offset + i * OUTPLUSONE const tx = this.txs.get(nextOffset) diff --git a/zp-relayer/utils/constants.ts b/zp-relayer/utils/constants.ts index 13c43fe4..1fc2b0e7 100644 --- a/zp-relayer/utils/constants.ts +++ b/zp-relayer/utils/constants.ts @@ -20,6 +20,8 @@ const constants = { PERMIT2_CONTRACT: '0x000000000022D473030F116dDEE9F6B43aC78BA3', INIT_ROOT: '11469701942666298368112882412133877458305516134926649826543144744382391691533', MOCK_CALLDATA: '0x' + 'ff'.repeat(BASE_CALLDATA_SIZE + RAW_TX_RLP_OVERHEAD), + ZERO_BYTE_GAS: 4, + NZERO_BYTE_GAS: 16, OP_GAS_ORACLE_ADDRESS: '0x420000000000000000000000000000000000000F', RETRY_CONFIG: { retries: 2, diff --git a/zp-relayer/utils/helpers.ts b/zp-relayer/utils/helpers.ts index d5b55779..4d2c52b3 100644 --- a/zp-relayer/utils/helpers.ts +++ b/zp-relayer/utils/helpers.ts @@ -248,3 +248,9 @@ export function getFileHash(path: string) { hash.update(buffer) return hash.digest('hex') } + +export function applyDenominator(n: BN, d: BN) { + return d.testn(255) + ? n.div(d.maskn(255)) + : n.mul(d) +} diff --git a/zp-relayer/validation/tx/validateTx.ts b/zp-relayer/validation/tx/validateTx.ts index 47bc5cae..6a0da120 100644 --- a/zp-relayer/validation/tx/validateTx.ts +++ b/zp-relayer/validation/tx/validateTx.ts @@ -8,13 +8,14 @@ import config from '@/configs/relayerConfig' import type { Limits, Pool } from '@/pool' import type { NullifierSet } from '@/state/nullifierSet' import { web3 } from '@/services/web3' -import { contractCallRetry, numToHex, truncateMemoTxPrefix, unpackSignature } from '@/utils/helpers' -import { ZERO_ADDRESS, MESSAGE_PREFIX_COMMON_V1 } from '@/utils/constants' +import { applyDenominator, contractCallRetry, numToHex, truncateMemoTxPrefix, unpackSignature } from '@/utils/helpers' +import { ZERO_ADDRESS, MESSAGE_PREFIX_COMMON_V1, MOCK_CALLDATA } from '@/utils/constants' import { getTxProofField, parseDelta } from '@/utils/proofInputs' import type { TxPayload } from '@/queue/poolTxQueue' import type { PoolState } from '@/state/PoolState' import { checkAssertion, TxValidationError, checkSize, checkScreener, checkCondition } from './common' import type { PermitRecover } from '@/utils/permit/types' +import type { FeeManager } from '@/services/fee' const ZERO = toBN(0) @@ -197,7 +198,7 @@ function checkMemoPrefix(memo: string, txType: TxType) { export async function validateTx( { txType, rawMemo, txProof, depositSignature }: TxPayload, pool: Pool, - requiredFee: BN, + feeManager: FeeManager, traceId?: string ) { await checkAssertion(() => checkMemoPrefix(rawMemo, txType)) @@ -222,28 +223,33 @@ export async function validateTx( await checkAssertion(() => checkNullifier(nullifier, pool.state.nullifiers)) await checkAssertion(() => checkNullifier(nullifier, pool.optimisticState.nullifiers)) await checkAssertion(() => checkTransferIndex(toBN(pool.optimisticState.getNextIndex()), delta.transferIndex)) - await checkAssertion(() => checkFee(fee, requiredFee)) await checkAssertion(() => checkProof(txProof, (p, i) => pool.verifyProof(p, i))) const tokenAmount = delta.tokenAmount const tokenAmountWithFee = tokenAmount.add(fee) const energyAmount = delta.energyAmount + let nativeConvert = false let userAddress: string if (txType === TxType.WITHDRAWAL) { checkCondition(tokenAmountWithFee.lte(ZERO) && energyAmount.lte(ZERO), 'Incorrect withdraw amounts') const { nativeAmount, receiver } = txData as TxData + const nativeAmountBN = toBN(nativeAmount) userAddress = web3.utils.bytesToHex(Array.from(receiver)) logger.info('Withdraw address: %s', userAddress) await checkAssertion(() => checkNonZeroWithdrawAddress(userAddress)) - await checkAssertion(() => checkNativeAmount(toBN(nativeAmount), tokenAmountWithFee.neg())) + await checkAssertion(() => checkNativeAmount(nativeAmountBN, tokenAmountWithFee.neg())) + + if (!nativeAmountBN.isZero()) { + nativeConvert = true + } } else if (txType === TxType.DEPOSIT || txType === TxType.PERMITTABLE_DEPOSIT) { checkCondition(tokenAmount.gt(ZERO) && energyAmount.eq(ZERO), 'Incorrect deposit amounts') checkCondition(depositSignature !== null, 'Deposit signature is required') - const requiredTokenAmount = tokenAmountWithFee.mul(pool.denominator) + const requiredTokenAmount = applyDenominator(tokenAmountWithFee, pool.denominator) userAddress = await getRecoveredAddress( txType, nullifier, @@ -262,6 +268,14 @@ export async function validateTx( throw new TxValidationError('Unsupported TxType') } + const requiredFee = await feeManager.estimateFee({ + txType, + nativeConvert, + txData: MOCK_CALLDATA + rawMemo + (depositSignature || ''), + }) + const denominatedFee = requiredFee.denominate(pool.denominator).getEstimate() + await checkAssertion(() => checkFee(fee, denominatedFee)) + const limits = await pool.getLimitsFor(userAddress) await checkAssertion(() => checkLimits(limits, delta.tokenAmount)) diff --git a/zp-relayer/workers/poolTxWorker.ts b/zp-relayer/workers/poolTxWorker.ts index 3da93b12..d6be1fb7 100644 --- a/zp-relayer/workers/poolTxWorker.ts +++ b/zp-relayer/workers/poolTxWorker.ts @@ -4,7 +4,7 @@ import { toBN } from 'web3-utils' import { web3 } from '@/services/web3' import { logger } from '@/services/appLogger' import { poolTxQueue, BatchTx, PoolTxResult, WorkerTx, WorkerTxType } from '@/queue/poolTxQueue' -import { TX_QUEUE_NAME, MOCK_CALLDATA } from '@/utils/constants' +import { TX_QUEUE_NAME } from '@/utils/constants' import { buildPrefixedMemo, waitForFunds, withErrorLog, withMutex } from '@/utils/helpers' import { pool } from '@/pool' import { sentTxQueue } from '@/queue/sentTxQueue' @@ -145,13 +145,7 @@ export async function createPoolTxWorker({ } else if (type === WorkerTxType.Normal) { const tx = payload as WorkerTx - const requiredFee = await feeManager.estimateFee({ - gasLimit: config.relayerGasLimit, - extraData: tx.rawMemo + (tx.depositSignature || ''), - }) - const denominatedFee = requiredFee.denominate(pool.denominator).getEstimate() - - await validateTx(tx, pool, denominatedFee, traceId) + await validateTx(tx, pool, feeManager, traceId) processResult = await buildTx(tx, treeProver, pool.optimisticState) } else { diff --git a/zp-relayer/workers/workerTypes.ts b/zp-relayer/workers/workerTypes.ts index 20571e84..3bdb9b98 100644 --- a/zp-relayer/workers/workerTypes.ts +++ b/zp-relayer/workers/workerTypes.ts @@ -1,4 +1,3 @@ -import type BN from 'bn.js' import type { Redis } from 'ioredis' import type { Mutex } from 'async-mutex' import type { TxManager } from '@/tx/TxManager' @@ -12,7 +11,7 @@ export interface IWorkerBaseConfig { } export interface IPoolWorkerConfig extends IWorkerBaseConfig { - validateTx: (tx: TxPayload, pool: Pool, requiredFee: BN, traceId?: string) => Promise + validateTx: (tx: TxPayload, pool: Pool, feeManager: FeeManager, traceId?: string) => Promise treeProver: IProver mutex: Mutex txManager: TxManager