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

refactor(relayer): Relocate profitability & fill execution #1331

Merged
merged 20 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 111 additions & 93 deletions src/relayer/Relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,126 +208,144 @@ export class Relayer {
return true;
}

async checkForUnfilledDepositsAndFill(sendSlowRelays = true): Promise<void> {
// Fetch all unfilled deposits, order by total earnable fee.
const { config } = this;
const { hubPoolClient, profitClient, spokePoolClients, tokenClient, multiCallerClient } = this.clients;

// Flush any pre-existing enqueued transactions that might not have been executed.
multiCallerClient.clearTransactionQueue();

// Fetch unfilled deposits and filter out deposits upfront before we compute the minimum deposit confirmation
// per chain, which is based on the deposit volume we could fill.
const unfilledDeposits = await this._getUnfilledDeposits();
computeRequiredDepositConfirmations(deposits: V3Deposit[]): { [chainId: number]: number } {
const { profitClient } = this.clients;
const { minDepositConfirmations } = this.config;

// Sum the total unfilled deposit amount per origin chain and set a MDC for that chain.
const unfilledDepositAmountsPerChain: { [chainId: number]: BigNumber } = unfilledDeposits.reduce(
(agg, { deposit }) => {
const unfilledAmountUsd = profitClient.getFillAmountInUsd(deposit, deposit.outputAmount);
agg[deposit.originChainId] = (agg[deposit.originChainId] ?? bnZero).add(unfilledAmountUsd);
return agg;
},
{}
);
const unfilledDepositAmountsPerChain: { [chainId: number]: BigNumber } = deposits.reduce((agg, deposit) => {
const unfilledAmountUsd = profitClient.getFillAmountInUsd(deposit, deposit.outputAmount);
agg[deposit.originChainId] = (agg[deposit.originChainId] ?? bnZero).add(unfilledAmountUsd);
return agg;
}, {});

// Sort thresholds in ascending order.
const minimumDepositConfirmationThresholds = Object.keys(config.minDepositConfirmations)
const minimumDepositConfirmationThresholds = Object.keys(minDepositConfirmations)
.filter((x) => x !== "default")
.sort((x, y) => Number(x) - Number(y));

// Set the MDC for each origin chain equal to lowest threshold greater than the unfilled USD deposit amount.
// If we can't find a threshold greater than the USD amount, then use the default.
const mdcPerChain = Object.fromEntries(
Object.entries(unfilledDepositAmountsPerChain).map(([chainId, unfilledAmount]) => {
const usdThreshold = minimumDepositConfirmationThresholds.find((_usdThreshold) => {
return (
toBNWei(_usdThreshold).gte(unfilledAmount) &&
isDefined(config.minDepositConfirmations[_usdThreshold][chainId])
);
});
const usdThreshold = minimumDepositConfirmationThresholds.find(
(usdThreshold) =>
toBNWei(usdThreshold).gte(unfilledAmount) && isDefined(minDepositConfirmations[usdThreshold][chainId])
);

// If no thresholds are greater than unfilled amount, then use fallback which should have largest MDCs.
return [chainId, config.minDepositConfirmations[usdThreshold ?? "default"][chainId]];
return [chainId, minDepositConfirmations[usdThreshold ?? "default"][chainId]];
})
);
this.logger.debug({
at: "Relayer::checkForUnfilledDepositsAndFill",
message: "Setting minimum deposit confirmation based on origin chain aggregate deposit amount",
unfilledDepositAmountsPerChain,
mdcPerChain,
minDepositConfirmations: config.minDepositConfirmations,
});

// Filter out deposits whose block time does not meet the minimum number of confirmations for the origin chain.
const confirmedUnfilledDeposits = unfilledDeposits
.filter(
({ deposit: { originChainId, blockNumber } }) =>
blockNumber <= spokePoolClients[originChainId].latestBlockSearched - mdcPerChain[originChainId]
)
.map(({ deposit }) => deposit);
this.logger.debug({
at: "Relayer::checkForUnfilledDepositsAndFill",
message: `${confirmedUnfilledDeposits.length} unfilled deposits found`,
minDepositConfirmations,
});
return mdcPerChain;
}

// Iterate over all unfilled deposits. For each unfilled deposit: a) check that the token balance client has enough
// balance to fill the unfilled amount. b) the fill is profitable. If both hold true then fill the unfilled amount.
// If not enough ballance add the shortfall to the shortfall tracker to produce an appropriate log. If the deposit
// is has no other fills then send a 0 sized fill to initiate a slow relay. If unprofitable then add the
// unprofitable tx to the unprofitable tx tracker to produce an appropriate log.
const { slowDepositors } = config;
for (const deposit of confirmedUnfilledDeposits) {
const { depositor, recipient, destinationChainId, originChainId, inputToken, outputAmount } = deposit;

// If depositor is on the slow deposit list, then send a zero fill to initiate a slow relay and return early.
if (slowDepositors?.includes(depositor)) {
if (sendSlowRelays) {
this.logger.debug({
at: "Relayer",
message: "Initiating slow fill for grey listed depositor",
depositor,
});
this.requestSlowFill(deposit);
}
// Regardless of whether we should send a slow fill or not for this depositor, exit early at this point
// so we don't fast fill an already slow filled deposit from the slow fill-only list.
continue;
}
// Iterate over all unfilled deposits. For each unfilled deposit, check that:
// a) it exceeds the minimum number of required block confirmations,
// b) the token balance client has enough tokens to fill it,
// c) the fill is profitable.
// If all hold true then complete the fill. Otherwise, if slow fills are enabled, request a slow fill.
async evaluateFill(deposit: V3DepositWithBlock, maxBlockNumber: number, sendSlowRelays: boolean): Promise<void> {
const { depositId, depositor, recipient, destinationChainId, originChainId, inputToken, outputAmount } = deposit;
const { hubPoolClient, profitClient, tokenClient } = this.clients;
const { slowDepositors } = this.config;

// If the deposit does not meet the minimum number of block confirmations, skip it.
if (deposit.blockNumber > maxBlockNumber) {
const chain = getNetworkName(originChainId);
this.logger.debug({
at: "Relayer",
message: `Skipping ${chain} deposit ${depositId} due to insufficient deposit confirmations.`,
depositId,
transactionHash: deposit.transactionHash,
});
return;
}

const l1Token = hubPoolClient.getL1TokenInfoForL2Token(inputToken, originChainId);
const selfRelay = [depositor, recipient].every((address) => address === this.relayerAddress);
if (tokenClient.hasBalanceForFill(deposit, outputAmount) && !selfRelay) {
const {
repaymentChainId,
realizedLpFeePct,
relayerFeePct,
gasLimit: _gasLimit,
gasCost,
} = await this.resolveRepaymentChain(deposit, l1Token);
if (isDefined(repaymentChainId)) {
const gasLimit = isMessageEmpty(resolveDepositMessage(deposit)) ? undefined : _gasLimit;
this.fillRelay(deposit, repaymentChainId, realizedLpFeePct, gasLimit);
} else {
profitClient.captureUnprofitableFill(deposit, realizedLpFeePct, relayerFeePct, gasCost);
}
} else if (selfRelay) {
const { realizedLpFeePct } = await hubPoolClient.computeRealizedLpFeePct({
...deposit,
paymentChainId: destinationChainId,
// If depositor is on the slow deposit list, then send a zero fill to initiate a slow relay and return early.
if (slowDepositors?.includes(depositor)) {
if (sendSlowRelays) {
this.logger.debug({
at: "Relayer",
message: "Initiating slow fill for grey listed depositor",
depositor,
});
this.requestSlowFill(deposit);
}
// Regardless of whether we should send a slow fill or not for this depositor, exit early at this point
// so we don't fast fill an already slow filled deposit from the slow fill-only list.
return;
}

// A relayer can fill its own deposit without an ERC20 transfer. Only bypass profitability requirements if the
// relayer is both the depositor and the recipient, because a deposit on a cheap SpokePool chain could cause
// expensive fills on (for example) mainnet.
this.fillRelay(deposit, destinationChainId, realizedLpFeePct);
const l1Token = hubPoolClient.getL1TokenInfoForL2Token(inputToken, originChainId);
const selfRelay = [depositor, recipient].every((address) => address === this.relayerAddress);
if (tokenClient.hasBalanceForFill(deposit, outputAmount) && !selfRelay) {
const {
repaymentChainId,
realizedLpFeePct,
relayerFeePct,
gasLimit: _gasLimit,
gasCost,
} = await this.resolveRepaymentChain(deposit, l1Token);
if (isDefined(repaymentChainId)) {
const gasLimit = isMessageEmpty(resolveDepositMessage(deposit)) ? undefined : _gasLimit;
this.fillRelay(deposit, repaymentChainId, realizedLpFeePct, gasLimit);
} else {
// TokenClient.getBalance returns that we don't have enough balance to submit the fast fill.
// At this point, capture the shortfall so that the inventory manager can rebalance the token inventory.
tokenClient.captureTokenShortfallForFill(deposit, outputAmount);
if (sendSlowRelays) {
this.requestSlowFill(deposit);
}
profitClient.captureUnprofitableFill(deposit, realizedLpFeePct, relayerFeePct, gasCost);
}
} else if (selfRelay) {
const { realizedLpFeePct } = await hubPoolClient.computeRealizedLpFeePct({
...deposit,
paymentChainId: destinationChainId,
});

// A relayer can fill its own deposit without an ERC20 transfer. Only bypass profitability requirements if the
// relayer is both the depositor and the recipient, because a deposit on a cheap SpokePool chain could cause
// expensive fills on (for example) mainnet.
this.fillRelay(deposit, destinationChainId, realizedLpFeePct);
} else {
// TokenClient.getBalance returns that we don't have enough balance to submit the fast fill.
// At this point, capture the shortfall so that the inventory manager can rebalance the token inventory.
tokenClient.captureTokenShortfallForFill(deposit, outputAmount);
if (sendSlowRelays) {
this.requestSlowFill(deposit);
}
}
}

async checkForUnfilledDepositsAndFill(sendSlowRelays = true): Promise<void> {
// Fetch all unfilled deposits, order by total earnable fee.
const { profitClient, spokePoolClients, tokenClient, multiCallerClient } = this.clients;

// Flush any pre-existing enqueued transactions that might not have been executed.
multiCallerClient.clearTransactionQueue();

// Fetch unfilled deposits and filter out deposits upfront before we compute the minimum deposit confirmation
// per chain, which is based on the deposit volume we could fill.
const unfilledDeposits = await this._getUnfilledDeposits();
const allUnfilledDeposits = Object.values(unfilledDeposits.map(({ deposit }) => deposit));
this.logger.debug({
at: "Relayer#checkForUnfilledDepositsAndFill",
message: `${allUnfilledDeposits.length} unfilled deposits found.`,
});
if (allUnfilledDeposits.length === 0) {
return;
}

const mdcPerChain = this.computeRequiredDepositConfirmations(allUnfilledDeposits);
for (const deposit of allUnfilledDeposits) {
const { originChainId } = deposit;
const maxBlockNumber = spokePoolClients[originChainId].latestBlockSearched - mdcPerChain[originChainId];
await this.evaluateFill(deposit, maxBlockNumber, sendSlowRelays);
}

// If during the execution run we had shortfalls or unprofitable fills then handel it by producing associated logs.
if (tokenClient.anyCapturedShortFallFills()) {
this.handleTokenShortfall();
Expand Down
2 changes: 1 addition & 1 deletion test/Relayer.BasicFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {

await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(lastSpyLogIncludes(spy, "0 unfilled deposits")).to.be.true;
expect(lastSpyLogIncludes(spy, "due to insufficient deposit confirmations")).to.be.true;
});

it("Ignores deposits with quote times in future", async function () {
Expand Down
Loading