Skip to content

Commit

Permalink
feat(relayer): Propagate estimated gas cost for fills (#982)
Browse files Browse the repository at this point in the history
When estimating relay profitability, pass the computed gasLimit back to
the caller. This is a prerequisite for dynamically computing the cost of
filling deposits with messages, and should incidentally save doing the
fill estimate later on.
  • Loading branch information
pxrl authored Oct 24, 2023
1 parent 97fe05b commit eca98ad
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 49 deletions.
79 changes: 46 additions & 33 deletions src/clients/ProfitClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@ import { Deposit, DepositWithBlock, L1Token, SpokePoolClientsByChain } from "../
import { HubPoolClient } from ".";

const { formatEther } = ethersUtils;

const { EMPTY_MESSAGE, DEFAULT_SIMULATED_RELAYER_ADDRESS: TEST_RELAYER } = sdkConsts;
const { bnOne, bnUint32Max, fixedPointAdjustment: fixedPoint, isMessageEmpty, resolveDepositMessage } = sdkUtils;
const {
bnOne,
bnUint32Max,
bnUint256Max: uint256Max,
fixedPointAdjustment: fixedPoint,
isMessageEmpty,
resolveDepositMessage,
} = sdkUtils;

// We use wrapped ERC-20 versions instead of the native tokens such as ETH, MATIC for ease of computing prices.
export const MATIC = TOKEN_SYMBOLS_MAP.MATIC.addresses[CHAIN_IDs.MAINNET];
export const USDC = TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET];
export const WBTC = TOKEN_SYMBOLS_MAP.WBTC.addresses[CHAIN_IDs.MAINNET];
export const WETH = TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET];

// note: All FillProfit BigNumbers are scaled to 18 decimals unless specified otherwise.
// @note All FillProfit BigNumbers are scaled to 18 decimals unless specified otherwise.
export type FillProfit = {
grossRelayerFeePct: BigNumber; // Max of relayerFeePct and newRelayerFeePct from Deposit.
tokenPriceUsd: BigNumber; // Resolved USD price of the bridged token.
Expand All @@ -46,6 +54,12 @@ export type FillProfit = {
profitable: boolean; // Fill profitability indicator.
};

type UnprofitableFill = {
deposit: DepositWithBlock;
fillAmount: BigNumber;
nativeGasCost: BigNumber;
};

export const GAS_TOKEN_BY_CHAIN_ID: { [chainId: number]: string } = {
1: WETH,
10: WETH,
Expand Down Expand Up @@ -99,7 +113,7 @@ export class ProfitClient {
private readonly priceClient;
protected minRelayerFees: { [route: string]: BigNumber } = {};
protected tokenPrices: { [l1Token: string]: BigNumber } = {};
private unprofitableFills: { [chainId: number]: { deposit: DepositWithBlock; fillAmount: BigNumber }[] } = {};
private unprofitableFills: { [chainId: number]: UnprofitableFill[] } = {};

// Track total gas costs of a relay on each chain.
protected totalGasCosts: { [chainId: number]: BigNumber } = {};
Expand Down Expand Up @@ -171,11 +185,7 @@ export class ProfitClient {
}

// Estimate the gas cost of filling this relay.
estimateFillCost(deposit: Deposit): {
nativeGasCost: BigNumber;
gasPriceUsd: BigNumber;
gasCostUsd: BigNumber;
} {
estimateFillCost(deposit: Deposit): Pick<FillProfit, "nativeGasCost" | "gasPriceUsd" | "gasCostUsd"> {
const { destinationChainId: chainId } = deposit;
const gasPriceUsd = this.getPriceOfToken(GAS_TOKEN_BY_CHAIN_ID[chainId]);
const nativeGasCost = this.getTotalGasCost(deposit); // gas cost in native token
Expand Down Expand Up @@ -305,27 +315,10 @@ export class ProfitClient {
return fillAmount.mul(tokenPriceInUsd).div(toBN(10).pow(l1TokenInfo.decimals));
}

getFillProfitability(
deposit: Deposit,
fillAmount: BigNumber,
refundFee: BigNumber,
l1Token: L1Token
): FillProfit | undefined {
getFillProfitability(deposit: Deposit, fillAmount: BigNumber, refundFee: BigNumber, l1Token: L1Token): FillProfit {
const minRelayerFeePct = this.minRelayerFeePct(l1Token.symbol, deposit.originChainId, deposit.destinationChainId);
let fill: FillProfit;

try {
fill = this.calculateFillProfitability(deposit, fillAmount, refundFee, l1Token, minRelayerFeePct);
} catch (err) {
this.logger.debug({
at: "ProfitClient#isFillProfitable",
message: `Unable to determine fill profitability (${err}).`,
deposit,
fillAmount,
});
return undefined;
}

const fill = this.calculateFillProfitability(deposit, fillAmount, refundFee, l1Token, minRelayerFeePct);
if (!fill.profitable || this.debugProfitability) {
const { depositId, originChainId } = deposit;
const profitable = fill.profitable ? "profitable" : "unprofitable";
Expand Down Expand Up @@ -354,14 +347,34 @@ export class ProfitClient {
return fill;
}

isFillProfitable(deposit: Deposit, fillAmount: BigNumber, refundFee: BigNumber, l1Token: L1Token): boolean {
const { profitable } = this.getFillProfitability(deposit, fillAmount, refundFee, l1Token);
return profitable ?? this.isTestnet;
isFillProfitable(
deposit: Deposit,
fillAmount: BigNumber,
refundFee: BigNumber,
l1Token: L1Token
): Pick<FillProfit, "profitable" | "nativeGasCost"> {
let profitable = false;
let nativeGasCost = uint256Max;
try {
({ profitable, nativeGasCost } = this.getFillProfitability(deposit, fillAmount, refundFee, l1Token));
} catch (err) {
this.logger.debug({
at: "ProfitClient#isFillProfitable",
message: `Unable to determine fill profitability (${err}).`,
deposit,
fillAmount,
});
}

return {
profitable: profitable || this.isTestnet,
nativeGasCost,
};
}

captureUnprofitableFill(deposit: DepositWithBlock, fillAmount: BigNumber): void {
this.logger.debug({ at: "ProfitClient", message: "Handling unprofitable fill", deposit, fillAmount });
assign(this.unprofitableFills, [deposit.originChainId], [{ deposit, fillAmount }]);
captureUnprofitableFill(deposit: DepositWithBlock, fillAmount: BigNumber, gasCost: BigNumber): void {
this.logger.debug({ at: "ProfitClient", message: "Handling unprofitable fill", deposit, fillAmount, gasCost });
assign(this.unprofitableFills, [deposit.originChainId], [{ deposit, fillAmount, gasCost }]);
}

anyCapturedUnprofitableFills(): boolean {
Expand Down
33 changes: 21 additions & 12 deletions src/relayer/Relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,16 @@ export class Relayer {
// The SpokePool guarantees the sum of the fees is <= 100% of the deposit amount.
deposit.realizedLpFeePct = await this.computeRealizedLpFeePct(version, deposit);

const repaymentChainId = await this.resolveRepaymentChain(version, deposit, unfilledAmount, l1Token);
const { repaymentChainId, gasLimit: gasCost } = await this.resolveRepaymentChain(
version,
deposit,
unfilledAmount,
l1Token
);
if (isDefined(repaymentChainId)) {
this.fillRelay(deposit, unfilledAmount, repaymentChainId);
} else {
profitClient.captureUnprofitableFill(deposit, unfilledAmount);
profitClient.captureUnprofitableFill(deposit, unfilledAmount, gasCost);
}
} else {
tokenClient.captureTokenShortfallForFill(deposit, unfilledAmount);
Expand All @@ -288,7 +293,7 @@ export class Relayer {
}
}

fillRelay(deposit: Deposit, fillAmount: BigNumber, repaymentChainId: number): void {
fillRelay(deposit: Deposit, fillAmount: BigNumber, repaymentChainId: number, gasLimit?: BigNumber): void {
// Skip deposits that this relayer has already filled completely before to prevent double filling (which is a waste
// of gas as the second fill would fail).
// TODO: Handle the edge case scenario where the first fill failed due to transient errors and needs to be retried
Expand Down Expand Up @@ -329,6 +334,7 @@ export class Relayer {
chainId: deposit.destinationChainId,
method,
args: argBuilder(deposit, repaymentChainId, fillAmount),
gasLimit,
message,
mrkdwn: this.constructRelayFilledMrkdwn(deposit, repaymentChainId, fillAmount),
});
Expand Down Expand Up @@ -508,7 +514,7 @@ export class Relayer {
deposit: DepositWithBlock,
fillAmount: BigNumber,
hubPoolToken: L1Token
): Promise<number | undefined> {
): Promise<{ repaymentChainId?: number; gasLimit: BigNumber }> {
const { depositId, originChainId, destinationChainId, transactionHash: depositHash } = deposit;
const { inventoryClient, profitClient } = this.clients;

Expand All @@ -520,14 +526,19 @@ export class Relayer {
message: `Skipping repayment chain determination for partial fill on ${destinationChain}`,
deposit: { originChain, depositId, destinationChain, depositHash },
});
return destinationChainId;
}

const preferredChainId = await inventoryClient.determineRefundChainId(deposit, hubPoolToken.address);
const preferredChainId = fillAmount.eq(deposit.amount)
? await inventoryClient.determineRefundChainId(deposit, hubPoolToken.address)
: destinationChainId;

const refundFee = this.computeRefundFee(version, deposit);
const { profitable, nativeGasCost } = profitClient.isFillProfitable(deposit, fillAmount, refundFee, hubPoolToken);

const profitable = profitClient.isFillProfitable(deposit, fillAmount, refundFee, hubPoolToken);
return profitable ? preferredChainId : undefined;
return {
repaymentChainId: profitable ? preferredChainId : undefined,
gasLimit: nativeGasCost,
};
}

protected async computeRealizedLpFeePct(version: number, deposit: DepositWithBlock): Promise<BigNumber> {
Expand Down Expand Up @@ -608,15 +619,13 @@ export class Relayer {
Object.keys(unprofitableDeposits).forEach((chainId) => {
let depositMrkdwn = "";
Object.keys(unprofitableDeposits[chainId]).forEach((depositId) => {
const unprofitableDeposit = unprofitableDeposits[chainId][depositId];
const deposit: DepositWithBlock = unprofitableDeposit.deposit;
const fillAmount: BigNumber = unprofitableDeposit.fillAmount;
const { deposit, fillAmount, gasCost: _gasCost } = unprofitableDeposits[chainId][depositId];
// Skip notifying if the unprofitable fill happened too long ago to avoid spamming.
if (deposit.quoteTimestamp + UNPROFITABLE_DEPOSIT_NOTICE_PERIOD < getCurrentTime()) {
return;
}
const gasCost = _gasCost.toString();

const gasCost = this.clients.profitClient.getTotalGasCost(deposit).toString();
const { symbol, decimals } = this.clients.hubPoolClient.getTokenInfoForDeposit(deposit);
const formatFunction = createFormatFunction(2, 4, false, decimals);
const gasFormatFunction = createFormatFunction(2, 10, false, 18);
Expand Down
10 changes: 6 additions & 4 deletions test/ProfitClient.ConsiderProfitability.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { random } from "lodash";
import { assert } from "chai";
import { constants as sdkConstants, utils as sdkUtils } from "@across-protocol/sdk-v2";
import {
Expand Down Expand Up @@ -314,7 +315,7 @@ describe("ProfitClient: Consider relay profit", () => {
netRelayerFeeUsd: expected.netRelayerFeeUsd,
});

const profitable = profitClient.isFillProfitable(deposit, nativeFillAmount, zeroRefundFee, l1Token);
const { profitable } = profitClient.isFillProfitable(deposit, nativeFillAmount, zeroRefundFee, l1Token);
expect(profitable).to.equal(expected.profitable);
});
});
Expand Down Expand Up @@ -367,7 +368,7 @@ describe("ProfitClient: Consider relay profit", () => {
netRelayerFeeUsd: formatEther(expected.netRelayerFeeUsd),
});

const profitable = profitClient.isFillProfitable(deposit, nativeFillAmount, nativeRefundFee, l1Token);
const { profitable } = profitClient.isFillProfitable(deposit, nativeFillAmount, nativeRefundFee, l1Token);
expect(profitable).to.equal(expected.profitable);
});
});
Expand Down Expand Up @@ -453,7 +454,8 @@ describe("ProfitClient: Consider relay profit", () => {

it("Captures unprofitable fills", () => {
const deposit = { relayerFeePct: toBNWei("0.003"), originChainId: 1, depositId: 42 } as DepositWithBlock;
profitClient.captureUnprofitableFill(deposit, toBNWei(1));
expect(profitClient.getUnprofitableFills()).to.deep.equal({ 1: [{ deposit, fillAmount: toBNWei(1) }] });
const gasCost = toGWei(random(1, 100_000));
profitClient.captureUnprofitableFill(deposit, toBNWei(1), gasCost);
expect(profitClient.getUnprofitableFills()).to.deep.equal({ 1: [{ deposit, fillAmount: toBNWei(1), gasCost }] });
});
});

0 comments on commit eca98ad

Please sign in to comment.