Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fee manager improvements #187

Merged
merged 14 commits into from
Jul 14, 2023
6 changes: 6 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions zp-relayer/configs/relayerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'),
lok52 marked this conversation as resolved.
Show resolved Hide resolved
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',
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion zp-relayer/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion zp-relayer/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 2 additions & 2 deletions zp-relayer/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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),
}
Expand Down
2 changes: 1 addition & 1 deletion zp-relayer/queue/poolTxQueue.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
30 changes: 16 additions & 14 deletions zp-relayer/services/fee/DynamicFeeManager.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<DynamicFeeOptions> {
const gasPrice = await this.gasPrice.fetchOnce()
const oneByteFee = FeeManager.executionFee(gasPrice, toBN(NZERO_BYTE_GAS))
return DynamicFeeOptions.fromGasPice(gasPrice, oneByteFee, relayerConfig.minBaseFee)
}
}
161 changes: 118 additions & 43 deletions zp-relayer/services/fee/FeeManager.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
[key: string]: T | NestedRecord<T>
}

export interface IUserFeeOptions {
type Fees<K extends string[], V = BN> = { [k in K[number]]: V }
export interface IFeeOptions<K extends string[], V = BN> {
fees: Fees<K, V>
applyFactor(factor: BN): this
applyMinBound(): this
denominate(denominator: BN): this
convert(priceFeed: IPriceFeed): Promise<this>
getObject(): Record<string, string>
getObject(): NestedRecord<string>
clone(): this
}

export class DefaultUserFeeOptions implements IUserFeeOptions {
constructor(protected fee: BN) {}
export class FeeOptions<T extends string[]> implements IFeeOptions<T, BN> {
constructor(public fees: Fees<T, BN>, private readonly minFees?: Fees<T, BN>) {}

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<V>(f: (v: BN, k: T[number]) => V) {
const clone = {} as Fees<T, V>
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<string> {
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<DynamicFeeKeys> {
static fromGasPice(gasPrice: GasPriceValue, oneByteFee: BN, minFee: BN) {
const getFee = (txType: TxType) => FeeManager.executionFee(gasPrice, config.baseTxGas[txType])
const fees: Fees<DynamicFeeKeys> = {
[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<DynamicFeeKeys> = {
[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
}
}

Expand All @@ -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<T extends string[] = DynamicFeeKeys> {
private cachedFeeOptions: IFeeOptions<T> | null = null
private updateFeeOptionsInterval: NodeJS.Timeout | null = null

constructor(protected config: IFeeManagerConfig) {}
Expand All @@ -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(),
Expand All @@ -87,49 +167,44 @@ export abstract class FeeManager {
}, this.config.updateInterval)
}

static async estimateExecutionFee(gasPrice: GasPrice<EstimationType>, gasLimit: BN): Promise<BN> {
const price = await gasPrice.fetchOnce()
return toBN(getMaxRequiredGasPrice(price)).mul(gasLimit)
}

private async convertAndScale<T extends IUserFeeOptions>(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<FeeEstimate> {
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<IUserFeeOptions> {
const feeOptions = await this._fetchFeeOptions(params)
const convertedFees = await this.convertAndScale(feeOptions)
async fetchFeeOptions(): Promise<IFeeOptions<T>> {
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<IUserFeeOptions> {
async getFeeOptions(useCached = true): Promise<IFeeOptions<T>> {
if (useCached && this.cachedFeeOptions) return this.cachedFeeOptions.clone()
let feeOptions: IUserFeeOptions
let feeOptions: IFeeOptions<T>
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)
if (!this.cachedFeeOptions) throw e
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<FeeEstimate>
protected abstract _estimateFee(params: IFeeEstimateParams, fees: IFeeOptions<T>): Promise<FeeEstimate>

// Should provide fee estimations for users
protected abstract _fetchFeeOptions(params: IGetFeesParams): Promise<IUserFeeOptions>
protected abstract _fetchFeeOptions(): Promise<IFeeOptions<T>>
}
Loading