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
1 change: 1 addition & 0 deletions zp-relayer/configs/relayerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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),
baseTxGas: toBN(process.env.RELAYER_BASE_TX_GAS as string),
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
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({ gasLimit: config.baseTxGas })
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,7 @@ function buildFeeManager(
scaleFactor: config.feeScalingFactor,
marginFactor: config.feeMarginFactor,
updateInterval: config.feeManagerUpdateInterval,
defaultFeeOptionsParams: { gasLimit: config.relayerGasLimit },
defaultFeeOptionsParams: { gasLimit: config.baseTxGas },
}
if (type === FeeManagerType.Static) {
if (config.relayerFee === null) throw new Error('Static relayer fee is not set')
Expand Down
24 changes: 17 additions & 7 deletions zp-relayer/services/fee/DynamicFeeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { toBN } from 'web3-utils'
import {
FeeManager,
FeeEstimate,
DefaultUserFeeOptions,
UserFeeOptions,
IFeeEstimateParams,
IGetFeesParams,
IFeeManagerConfig,
DynamicFeeOptions,
} from './FeeManager'
import type { EstimationType, GasPrice } from '../gas-price'
import { NZERO_BYTE_GAS } from '@/utils/constants'

export class DynamicFeeManager extends FeeManager {
constructor(config: IFeeManagerConfig, private gasPrice: GasPrice<EstimationType>) {
Expand All @@ -16,13 +18,21 @@ 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({ txData }: IFeeEstimateParams, feeOptions: DynamicFeeOptions) {
const { fee: baseFee, oneByteFee } = feeOptions.fees
// -1 to account for the 0x prefix
const calldataLen = txData.length >> (1 - 1)
lok52 marked this conversation as resolved.
Show resolved Hide resolved
const fee = baseFee.add(oneByteFee.muln(calldataLen))
return new FeeEstimate({ fee })
}

async _fetchFeeOptions({ gasLimit }: IGetFeesParams) {
const baseFee = await FeeManager.estimateExecutionFee(this.gasPrice, gasLimit)
return new DefaultUserFeeOptions(baseFee)
async _fetchFeeOptions({ gasLimit }: IGetFeesParams): Promise<DynamicFeeOptions> {
const gasPrice = await this.gasPrice.fetchOnce()
const fee = FeeManager.executionFee(gasPrice, gasLimit)
const oneByteFee = FeeManager.executionFee(gasPrice, toBN(NZERO_BYTE_GAS))
return new UserFeeOptions({
fee,
oneByteFee,
})
}
}
53 changes: 35 additions & 18 deletions zp-relayer/services/fee/FeeManager.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,76 @@
import type BN from 'bn.js'
import { toBN } from 'web3-utils'
import type { IPriceFeed } from '../price-feed/IPriceFeed'
import { GasPrice, EstimationType, getMaxRequiredGasPrice } from '../gas-price'
import { getMaxRequiredGasPrice, GasPriceValue } from '../gas-price'
import { setIntervalAndRun } from '@/utils/helpers'
import { logger } from '../appLogger'

export interface IGetFeesParams {
gasLimit: BN
}
export interface IFeeEstimateParams extends IGetFeesParams {
extraData: string
txData: string
}

export interface IUserFeeOptions {
applyFactor(factor: BN): this
denominate(denominator: BN): this
convert(priceFeed: IPriceFeed): Promise<this>
getObject(): Record<string, string>
clone(): this
clone(): IUserFeeOptions
}

export class DefaultUserFeeOptions implements IUserFeeOptions {
constructor(protected fee: BN) {}
type FeeOptions<K extends string, V = BN> = Required<Record<K, V>>

export class UserFeeOptions<T extends string> implements IUserFeeOptions {
constructor(public fees: FeeOptions<T>) {}

private mapI(f: (_: BN) => BN) {
for (const k in this.fees) {
this.fees[k] = f(this.fees[k])
}
}

private map<V>(f: (_: BN) => V): FeeOptions<T, V> {
const clone = {} as any
for (const k in this.fees) {
clone[k] = f(this.fees[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)
this.mapI(p => p.div(denominator))
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
}

clone() {
return new DefaultUserFeeOptions(this.fee.clone()) as this
const cloneBN = (p: BN) => p.clone()
return new UserFeeOptions(this.map(cloneBN))
}

getObject() {
return {
fee: this.fee.toString(10),
}
return this.map(p => p.toString(10))
}
}

export class FeeEstimate extends DefaultUserFeeOptions {
export type DynamicFeeOptions = UserFeeOptions<'fee' | 'oneByteFee'>

// Utility class for internal fee estimations
export class FeeEstimate extends UserFeeOptions<'fee'> {
getEstimate() {
return this.fee
return this.fees.fee
}
}

Expand Down Expand Up @@ -87,9 +105,8 @@ 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)
static executionFee(gasPrice: GasPriceValue, gasLimit: BN): BN {
return toBN(getMaxRequiredGasPrice(gasPrice)).mul(gasLimit)
}

private async convertAndScale<T extends IUserFeeOptions>(baseFee: T) {
Expand Down
63 changes: 16 additions & 47 deletions zp-relayer/services/fee/OptimismFeeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,12 @@ import {
FeeEstimate,
IFeeEstimateParams,
IFeeManagerConfig,
IUserFeeOptions,
IGetFeesParams,
UserFeeOptions,
DynamicFeeOptions,
} from './FeeManager'
import type { IPriceFeed } from '../price-feed'
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),
}
}
}
import { ZERO_BYTE_GAS, NZERO_BYTE_GAS } from '@/utils/constants'

export class OptimismFeeManager extends FeeManager {
private oracle: Contract
Expand Down Expand Up @@ -87,28 +51,33 @@ export class OptimismFeeManager extends FeeManager {
return scaled
}

async _estimateFee({ extraData }: IFeeEstimateParams, feeOptions: OptimismUserFeeOptions) {
async _estimateFee({ txData }: IFeeEstimateParams, feeOptions: DynamicFeeOptions) {
const { fee: baseFee, oneByteFee } = feeOptions.getObject()

const unscaledL1Fee = this.getL1Fee(MOCK_CALLDATA + extraData, toBN(oneByteFee))
const unscaledL1Fee = this.getL1Fee(txData, toBN(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)
return new FeeEstimate({
fee: feeEstimate,
})
}

async _fetchFeeOptions({ gasLimit }: IGetFeesParams): Promise<OptimismUserFeeOptions> {
// TODO: add RLP encoding overhead to baseFee
const baseFee = await FeeManager.estimateExecutionFee(this.gasPrice, gasLimit)
async _fetchFeeOptions({ gasLimit }: IGetFeesParams): Promise<DynamicFeeOptions> {
const gasPrice = await this.gasPrice.fetchOnce()
const baseFee = FeeManager.executionFee(gasPrice, gasLimit)

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 new UserFeeOptions({
fee: baseFee,
oneByteFee,
})
}
}
10 changes: 7 additions & 3 deletions zp-relayer/services/fee/StaticFeeManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type BN from 'bn.js'
import { FeeManager, FeeEstimate, DefaultUserFeeOptions, IFeeManagerConfig } from './FeeManager'
import { FeeManager, FeeEstimate, IFeeManagerConfig, UserFeeOptions } from './FeeManager'

export class StaticFeeManager extends FeeManager {
constructor(config: IFeeManagerConfig, private readonly staticFee: BN) {
Expand All @@ -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 UserFeeOptions({
fee: this.staticFee,
})
}
}
3 changes: 2 additions & 1 deletion zp-relayer/services/price-feed/IPriceFeed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type BN from 'bn.js'

export interface IPriceFeed {
convert(baseTokenAmounts: BN[]): Promise<BN[]>
getRate(): Promise<BN>
convert(rate: BN, baseTokenAmounts: BN): BN
}
9 changes: 7 additions & 2 deletions zp-relayer/services/price-feed/NativePriceFeed.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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<BN[]> {
return baseTokenAmounts
async getRate(): Promise<BN> {
return toBN(1)
}

convert(_: BN, baseTokenAmount: BN): BN {
return baseTokenAmount
}
}
28 changes: 13 additions & 15 deletions zp-relayer/services/price-feed/OneInchPriceFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,27 @@ export class OneInchPriceFeed implements IPriceFeed {
this.contract = new web3.eth.Contract(OracleAbi as AbiItem[], contractAddress)
}

async init() {
lok52 marked this conversation as resolved.
Show resolved Hide resolved
if (this.baseTokenAddress !== ZERO_ADDRESS) {
this.tokenDecimals = await this.getContractDecimals(this.baseTokenAddress)
} else {
this.tokenDecimals = toBN(toWei('1')) // 1 ether
}
}

private async getContractDecimals(contractAddress: string): Promise<BN> {
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<BN> {
if (!this.baseTokenDecimals) {
this.baseTokenDecimals =
this.baseTokenAddress !== ZERO_ADDRESS
? await this.getContractDecimals(this.baseTokenAddress)
: toBN(toWei('1')) // 1 ether
}
return this.baseTokenDecimals
getRate(): Promise<BN> {
return this.contract.methods.getRate(this.baseTokenAddress, this.poolTokenAddress, true).call().then(toBN)
}

async convert(baseTokenAmounts: BN[]): Promise<BN[]> {
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)
}
}
6 changes: 5 additions & 1 deletion zp-relayer/state/PoolState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions zp-relayer/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const constants = {
PERMIT2_CONTRACT: '0x000000000022D473030F116dDEE9F6B43aC78BA3',
INIT_ROOT: '11469701942666298368112882412133877458305516134926649826543144744382391691533',
MOCK_CALLDATA: '0x' + 'ff'.repeat(BASE_CALLDATA_SIZE + RAW_TX_RLP_OVERHEAD),
BASE_GAS_LIMIT: 650000,
ZERO_BYTE_GAS: 4,
NZERO_BYTE_GAS: 16,
OP_GAS_ORACLE_ADDRESS: '0x420000000000000000000000000000000000000F',
RETRY_CONFIG: {
retries: 2,
Expand Down
2 changes: 1 addition & 1 deletion zp-relayer/workers/poolTxWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export async function createPoolTxWorker({

const requiredFee = await feeManager.estimateFee({
gasLimit: config.relayerGasLimit,
extraData: tx.rawMemo + (tx.depositSignature || ''),
txData: MOCK_CALLDATA + tx.rawMemo + (tx.depositSignature || ''),
})
const denominatedFee = requiredFee.denominate(pool.denominator).getEstimate()

Expand Down