Skip to content

Commit

Permalink
refactor: Remove UBA RefundRequests (#1158)
Browse files Browse the repository at this point in the history
Refund requests are currently not part of v3 and will never be used in
v2. Flush them out to make the v3 updates easier.
  • Loading branch information
pxrl authored Jan 25, 2024
1 parent 95d4fce commit b2fba5c
Show file tree
Hide file tree
Showing 11 changed files with 11 additions and 752 deletions.
31 changes: 1 addition & 30 deletions src/clients/BundleDataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
FillsToRefund,
FillWithBlock,
ProposedRootBundle,
RefundRequestWithBlock,
UnfilledDeposit,
UnfilledDepositsForOriginChain,
} from "../interfaces";
Expand Down Expand Up @@ -32,7 +31,7 @@ import {
} from "../dataworker/DataworkerUtils";
import { getWidestPossibleExpectedBlockRange, isChainDisabled } from "../dataworker/PoolRebalanceUtils";
import { clients, typechain } from "@across-protocol/sdk-v2";
const { refundRequestIsValid, isUBAActivatedAtBlock } = clients;
const { isUBAActivatedAtBlock } = clients;

type DataCacheValue = {
unfilledDeposits: UnfilledDeposit[];
Expand Down Expand Up @@ -308,22 +307,6 @@ export class BundleDataClient {
(_blockRange, index) => this.chainIdListForBundleEvaluationBlockNumbers[index]
);

const validateRefundRequestAndSaveData = async (refundRequest: RefundRequestWithBlock): Promise<void> => {
const result = await refundRequestIsValid(spokePoolClients, this.clients.hubPoolClient, refundRequest);
if (result.valid) {
const { blockNumber, transactionIndex, transactionHash, logIndex, ...fill } = result.matchingFill;
assignValidFillToFillsToRefund(fillsToRefund, fill, refundRequest.repaymentChainId, refundRequest.refundToken);
updateTotalRefundAmount(fillsToRefund, fill, refundRequest.repaymentChainId, refundRequest.refundToken);
} else {
this.logger.warn({
at: "SpokePoolClient#validateRefundRequestAndSaveData",
message: "Invalid refund request",
refundRequest,
invalidReason: result.reason,
});
}
};

for (const originChainId of allChainIds) {
if (_isChainDisabled(originChainId)) {
continue;
Expand Down Expand Up @@ -399,18 +382,6 @@ export class BundleDataClient {
.map((fill) => validateFillAndSaveData(fill, blockRangeForChain))
);
}

// Handle fills that requested repayment on a different chain and submitted a refund request.
// These should map with only full fills where fill.destinationChainId !== fill.repaymentChainId.
if (isUBA) {
const blockRangeForChain = getBlockRangeForChain(
blockRangesForChains,
Number(originChainId),
this.chainIdListForBundleEvaluationBlockNumbers
);
const refundRequests = originClient.getRefundRequests(blockRangeForChain[0], blockRangeForChain[1]);
await Promise.all(refundRequests.map(async (refundRequest) => validateRefundRequestAndSaveData(refundRequest)));
}
}

// For each deposit with a matched fill, figure out the unfilled amount that we need to slow relay. We will filter
Expand Down
1 change: 0 additions & 1 deletion src/clients/ProfitClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ export class ProfitClient {
return price;
}

// @todo: Factor in the gas cost of submitting the RefundRequest on alt refund chains.
async getTotalGasCost(deposit: Deposit, fillAmount = deposit.amount): Promise<TransactionCostEstimate> {
const { destinationChainId: chainId } = deposit;

Expand Down
8 changes: 2 additions & 6 deletions src/clients/UBAClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { clients, interfaces } from "@across-protocol/sdk-v2";
import { FillWithBlock, RefundRequestWithBlock } from "../interfaces";
import { FillWithBlock } from "../interfaces";
import { HubPoolClient } from "./HubPoolClient";
import { SpokePoolClient } from "./SpokePoolClient";

const { getValidFillCandidates, getValidRefundCandidates } = clients;
const { getValidFillCandidates } = clients;
type SpokePoolFillFilter = clients.SpokePoolFillFilter;

export class UBAClient extends clients.UBAClient {
Expand All @@ -20,8 +20,4 @@ export class UBAClient extends clients.UBAClient {
async getFills(chainId: number, filter: SpokePoolFillFilter = {}): Promise<FillWithBlock[]> {
return getValidFillCandidates(chainId, this.spokePoolClients, filter);
}

async getRefundRequests(chainId: number, filter: SpokePoolFillFilter = {}): Promise<RefundRequestWithBlock[]> {
return getValidRefundCandidates(chainId, this.hubPoolClient, this.spokePoolClients, filter);
}
}
4 changes: 3 additions & 1 deletion src/dataworker/Dataworker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "assert";
import { utils as ethersUtils } from "ethers";
import {
winston,
Expand Down Expand Up @@ -746,7 +747,8 @@ export class Dataworker {
// repayment chain to pay out the refund. But we need to check which
// token should be repaid in.
if (isUbaOutflow(flow)) {
const refundToken = outflowIsFill(flow) ? flow.destinationToken : flow.refundToken;
assert(outflowIsFill(flow));
const refundToken = flow.destinationToken;
updateTotalRefundAmountRaw(fillsToRefund, balancingFee, flow.repaymentChainId, flow.relayer, refundToken);
}
});
Expand Down
3 changes: 0 additions & 3 deletions src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ export type FillWithBlock = interfaces.FillWithBlock;
export type SpeedUp = interfaces.SpeedUp;
export type SlowFill = interfaces.SlowFill;
export type SlowFillLeaf = interfaces.SlowFillLeaf;
export type RefundRequest = interfaces.RefundRequest;
export type RefundRequestWithBlock = interfaces.RefundRequestWithBlock;
export type RootBundleRelay = interfaces.RootBundleRelay;
export type RootBundleRelayWithBlock = interfaces.RootBundleRelayWithBlock;
export type RelayerRefundExecution = interfaces.RelayerRefundExecution;
Expand All @@ -71,6 +69,5 @@ export type UBASystemFee = clients.SystemFeeResult;
export const isUbaInflow = interfaces.isUbaInflow;
export const isUbaOutflow = interfaces.isUbaOutflow;
export const outflowIsFill = interfaces.outflowIsFill;
export const outflowIsRefund = interfaces.outflowIsRefund;

export type CachingMechanismInterface = interfaces.CachingMechanismInterface;
182 changes: 2 additions & 180 deletions src/relayer/Relayer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "assert";
import { utils as sdkUtils } from "@across-protocol/sdk-v2";
import { constants as ethersConstants, utils as ethersUtils } from "ethers";
import { Deposit, DepositWithBlock, FillWithBlock, L1Token, RefundRequestWithBlock } from "../interfaces";
import { utils as ethersUtils } from "ethers";
import { Deposit, DepositWithBlock, L1Token } from "../interfaces";
import {
BigNumber,
bnZero,
Expand All @@ -12,10 +12,8 @@ import {
buildFillRelayWithUpdatedFeeProps,
createFormatFunction,
formatFeePct,
getBlockForTimestamp,
getCurrentTime,
getNetworkName,
getRedisCache,
getUnfilledDeposits,
isDefined,
toBNWei,
Expand Down Expand Up @@ -463,164 +461,6 @@ export class Relayer {
this.fillRelay(deposit, zeroFillAmount, deposit.destinationChainId);
}

// Strategy for requesting refunds: Query all refunds requests, and match them to fills.
// The remaining fills are eligible for new requests.
async requestRefunds(sendRefundRequests = true): Promise<void> {
const { multiCallerClient, ubaClient } = this.clients;
assert(isDefined(ubaClient), "No ubaClient");

const spokePoolClients = Object.values(this.clients.spokePoolClients);
this.logger.debug({
at: "Relayer::requestRefunds",
message: "Evaluating chains for fills with outstanding cross-chain refunds",
chainIds: spokePoolClients.map(({ chainId }) => chainId),
});

const eligibleFillsByRefundChain: { [chainId: number]: FillWithBlock[] } = Object.fromEntries(
spokePoolClients.map(({ chainId }) => [chainId, []])
);

// Bound the event range by the relayer lookback. Must be resolved from an offset (in seconds) to a block number.
// Guard getBlockForTimestamp() by maxRelayerLookBack, because getBlockForTimestamp() doesn't work in test (yet).
let fromBlocks: { [chainId: number]: number } = {};
if (isDefined(this.config.maxRelayerLookBack)) {
const blockFinder = undefined;
const redis = await getRedisCache(this.logger);
const _fromBlocks = await Promise.all(
spokePoolClients.map((spokePoolClient) => {
const { chainId, deploymentBlock } = spokePoolClient;
const lookback = spokePoolClient.getCurrentTime() - this.config.maxRelayerLookBack;
return getBlockForTimestamp(chainId, lookback, blockFinder, redis) ?? deploymentBlock;
})
);
fromBlocks = Object.fromEntries(
_fromBlocks.map((blockNumber, idx) => [spokePoolClients[idx].chainId, blockNumber])
);
}

const refundRequests = await Promise.all(
spokePoolClients.map(({ chainId }) => {
const fromBlock = fromBlocks[chainId];
return ubaClient.getRefundRequests(chainId, { relayer: this.relayerAddress, fromBlock });
})
);

// For each refund/repayment Spoke Pool, group its set of refund requests by corresponding destination chain.
const refundRequestsByDstChain: { [chainId: number]: RefundRequestWithBlock[] } = Object.fromEntries(
spokePoolClients.map(({ chainId }) => [chainId, []])
);
refundRequests
.flat()
.forEach((refundRequest) => refundRequestsByDstChain[refundRequest.destinationChainId].push(refundRequest));

// For each destination Spoke Pool, find any fills that are eligible for a new refund request.
const fills = await Promise.all(
spokePoolClients.map(({ chainId: destinationChainId }) =>
this.findFillsWithoutRefundRequests(
destinationChainId,
refundRequestsByDstChain[destinationChainId],
fromBlocks[destinationChainId]
)
)
);
fills.flat().forEach((fill) => eligibleFillsByRefundChain[fill.repaymentChainId].push(fill));

// Enqueue a refund for each eligible fill.
let nRefunds = 0;
Object.values(eligibleFillsByRefundChain).forEach((fills) => {
nRefunds += fills.length;
fills.forEach((fill) => this.requestRefund(fill));
});

const message = `${nRefunds === 0 ? "No" : nRefunds} outstanding fills with eligible cross-chain refunds found.`;
const blockRanges = Object.fromEntries(
spokePoolClients.map(({ chainId, deploymentBlock, latestBlockSearched }) => {
return [chainId, [fromBlocks[chainId] ?? deploymentBlock, latestBlockSearched]];
})
);
this.logger.info({ at: "Relayer::requestRefunds", message, blockRanges });

await multiCallerClient.executeTransactionQueue(!sendRefundRequests);
}

async findFillsWithoutRefundRequests(
destinationChainId: number,
refundRequests: RefundRequestWithBlock[],
fromBlock: number
): Promise<FillWithBlock[]> {
const { ubaClient } = this.clients;
assert(isDefined(ubaClient), "No ubaClient");

const depositIds: { [chainId: number]: number[] } = {};

refundRequests.forEach(({ originChainId, depositId }) => {
depositIds[originChainId] ??= [];
depositIds[originChainId].push(depositId);
});

// Find fills where repayment was requested on another chain.
const filter = { relayer: this.relayerAddress, fromBlock };
const fills = (await ubaClient.getFills(destinationChainId, filter)).filter((fill) => {
const { depositId, originChainId, destinationChainId, repaymentChainId } = fill;
return repaymentChainId !== destinationChainId && !depositIds[originChainId]?.includes(depositId);
});

if (fills.length > 0) {
const fillsByChainId: { [chainId: number]: string[] } = {};
fills.forEach(({ destinationChainId, transactionHash }) => {
fillsByChainId[destinationChainId] ??= [];
fillsByChainId[destinationChainId].push(transactionHash);
});

this.logger.debug({
at: "Relayer::findFillsWithoutRefundRequests",
message: "Found fills eligible for cross-chain refund requests.",
fills: fillsByChainId,
});
}
return fills;
}

protected requestRefund(fill: FillWithBlock): void {
const { hubPoolClient, multiCallerClient, spokePoolClients } = this.clients;
const {
originChainId,
depositId,
destinationChainId,
destinationToken,
fillAmount: amount,
realizedLpFeePct,
repaymentChainId,
blockNumber: fillBlock,
} = fill;

const contract = spokePoolClients[repaymentChainId].spokePool;
const method = "requestRefund";
// @todo: Support specifying max impact against the refund amount (i.e. to mitigate price impact by fills).
const maxCount = ethersConstants.MaxUint256;

// Resolve the refund token from the fill token.
const hubPoolToken = hubPoolClient.getL1TokenForL2TokenAtBlock(destinationToken, destinationChainId);
const refundToken = hubPoolClient.getL2TokenForL1TokenAtBlock(hubPoolToken, repaymentChainId);

const args = [
refundToken,
amount,
originChainId,
destinationChainId,
realizedLpFeePct,
depositId,
fillBlock,
maxCount,
];

const message = `Submitted refund request on chain ${getNetworkName(repaymentChainId)}.`;
const mrkdwn = this.constructRefundRequestMarkdown(fill);

this.logger.debug({ at: "Relayer::requestRefund", message: "Requesting refund for fill.", fill });
multiCallerClient.enqueueTransaction({ chainId: repaymentChainId, contract, method, args, message, mrkdwn });
}

protected async resolveRepaymentChain(
version: number,
deposit: DepositWithBlock,
Expand Down Expand Up @@ -801,22 +641,4 @@ export class Relayer {

return msg;
}

private constructRefundRequestMarkdown(fill: FillWithBlock): string {
const { hubPoolClient } = this.clients;
const { depositId, destinationChainId, destinationToken } = fill;

const { symbol, decimals } = hubPoolClient.getL1TokenInfoForL2Token(destinationToken, destinationChainId);

const refundChain = getNetworkName(fill.repaymentChainId);
const originChain = getNetworkName(fill.originChainId);
const destinationChain = getNetworkName(destinationChainId);

const _fillAmount = createFormatFunction(2, 4, false, decimals)(fill.fillAmount.toString());

return (
`🌈 Requested refund on ${refundChain} for ${_fillAmount} ${symbol} for ${originChain} depositId ${depositId},` +
` filled on ${destinationChain}, with relayerFee ${formatFeePct(fill.relayerFeePct)}%.`
);
}
}
3 changes: 0 additions & 3 deletions src/relayer/RelayerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export class RelayerConfig extends CommonConfig {
readonly sendingRebalancesEnabled: boolean;
readonly sendingMessageRelaysEnabled: boolean;
readonly sendingSlowRelaysEnabled: boolean;
readonly sendingRefundRequestsEnabled: boolean;
readonly relayerTokens: string[];
readonly relayerOriginChains: number[] = [];
readonly relayerDestinationChains: number[] = [];
Expand Down Expand Up @@ -50,7 +49,6 @@ export class RelayerConfig extends CommonConfig {
SKIP_RELAYS,
SKIP_REBALANCING,
SEND_SLOW_RELAYS,
SEND_REFUND_REQUESTS,
MIN_RELAYER_FEE_PCT,
ACCEPT_INVALID_FILLS,
MIN_DEPOSIT_CONFIRMATIONS,
Expand Down Expand Up @@ -141,7 +139,6 @@ export class RelayerConfig extends CommonConfig {
this.sendingMessageRelaysEnabled = SEND_MESSAGE_RELAYS === "true";
this.skipRelays = SKIP_RELAYS === "true";
this.skipRebalancing = SKIP_REBALANCING === "true";
this.sendingRefundRequestsEnabled = SEND_REFUND_REQUESTS !== "false";
this.sendingSlowRelaysEnabled = SEND_SLOW_RELAYS === "true";
this.acceptInvalidFills = ACCEPT_INVALID_FILLS === "true";
(this.minDepositConfirmations = MIN_DEPOSIT_CONFIRMATIONS
Expand Down
12 changes: 0 additions & 12 deletions src/relayer/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { utils as sdkUtils } from "@across-protocol/sdk-v2";
import { processEndPollingLoop, winston, config, startupLogLevel, Signer, disconnectRedisClients } from "../utils";
import { Relayer } from "./Relayer";
import { RelayerConfig } from "./RelayerConfig";
Expand All @@ -13,10 +12,7 @@ export async function runRelayer(_logger: winston.Logger, baseSigner: Signer): P

try {
logger[startupLogLevel(config)]({ at: "Relayer#index", message: "Relayer started 🏃‍♂️", config });

relayerClients = await constructRelayerClients(logger, config, baseSigner);
const { configStoreClient } = relayerClients;

const relayer = new Relayer(await baseSigner.getAddress(), logger, relayerClients, config);

logger.debug({ at: "Relayer#index", message: "Relayer components initialized. Starting execution loop" });
Expand All @@ -25,15 +21,7 @@ export async function runRelayer(_logger: winston.Logger, baseSigner: Signer): P
await updateRelayerClients(relayerClients, config);

if (!config.skipRelays) {
// @note: For fills with a different repaymentChainId, refunds are requested on the _subsequent_ relayer run.
// Refunds requests are enqueued before new fills, so fillRelay simulation occurs closest to txn submission.
const version = configStoreClient.getConfigStoreVersionForTimestamp();
if (sdkUtils.isUBA(version) && version <= configStoreClient.configStoreVersion) {
await relayer.requestRefunds(config.sendingSlowRelaysEnabled);
}

await relayer.checkForUnfilledDepositsAndFill(config.sendingSlowRelaysEnabled);

await relayerClients.multiCallerClient.executeTransactionQueue(!config.sendingRelaysEnabled);
}

Expand Down
Loading

0 comments on commit b2fba5c

Please sign in to comment.