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

improve(relayer): Execute destination transactions async #1355

Merged
merged 11 commits into from
Apr 3, 2024
13 changes: 9 additions & 4 deletions src/clients/MultiCallerClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { utils as sdkUtils } from "@across-protocol/sdk-v2";
import { BigNumber } from "ethers";
import { DEFAULT_MULTICALL_CHUNK_SIZE, DEFAULT_CHAIN_MULTICALL_CHUNK_SIZE, Multicall2Call } from "../common";
import {
Expand Down Expand Up @@ -120,13 +121,17 @@ export class MultiCallerClient {
}

// For each chain, collate the enqueued transactions and process them in parallel.
async executeTxnQueues(simulate = false): Promise<Record<number, string[]>> {
const chainIds = [...new Set(Object.keys(this.valueTxns).concat(Object.keys(this.txns)))];
async executeTxnQueues(simulate = false, chainIds: number[] = []): Promise<Record<number, string[]>> {
if (chainIds.length === 0) {
chainIds = sdkUtils.dedupArray([
pxrl marked this conversation as resolved.
Show resolved Hide resolved
...Object.keys(this.valueTxns).map(Number),
...Object.keys(this.txns).map(Number),
]);
}

// One promise per chain for parallel execution.
const resultsByChain = await Promise.allSettled(
chainIds.map((_chainId) => {
const chainId = Number(_chainId);
chainIds.map((chainId) => {
const txns: AugmentedTransaction[] | undefined = this.txns[chainId];
const valueTxns: AugmentedTransaction[] | undefined = this.valueTxns[chainId];

Expand Down
10 changes: 8 additions & 2 deletions src/relayer/Relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ export class Relayer {
}
}

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

Expand Down Expand Up @@ -369,15 +369,21 @@ export class Relayer {
])
);

await sdkUtils.forEachAsync(Object.values(unfilledDeposits), async (unfilledDeposits) => {
await sdkUtils.forEachAsync(Object.entries(unfilledDeposits), async ([chainId, unfilledDeposits]) => {
if (unfilledDeposits.length === 0) {
return;
}

await this.evaluateFills(
unfilledDeposits.map(({ deposit }) => deposit),
maxBlockNumbers,
sendSlowRelays
);

const destinationChainId = Number(chainId);
if (multiCallerClient.getQueuedTransactions(destinationChainId).length > 0) {
await multiCallerClient.executeTxnQueues(simulate, [destinationChainId]);
pxrl marked this conversation as resolved.
Show resolved Hide resolved
}
});

// If during the execution run we had shortfalls or unprofitable fills then handel it by producing associated logs.
Expand Down
4 changes: 2 additions & 2 deletions src/relayer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export async function runRelayer(_logger: winston.Logger, baseSigner: Signer): P
await updateRelayerClients(relayerClients, config);

if (!config.skipRelays) {
await relayer.checkForUnfilledDepositsAndFill(config.sendingSlowRelaysEnabled);
await relayerClients.multiCallerClient.executeTransactionQueue(!config.sendingRelaysEnabled);
const simulate = !config.sendingRelaysEnabled;
await relayer.checkForUnfilledDepositsAndFill(config.sendingSlowRelaysEnabled, simulate);
}

// Unwrap WETH after filling deposits so we don't mess up slow fill logic, but before rebalancing
Expand Down
52 changes: 22 additions & 30 deletions test/Relayer.BasicFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
getLastBlockTime,
getRelayDataHash,
lastSpyLogIncludes,
spyLogIncludes,
randomAddress,
setupTokensForWallet,
sinon,
Expand Down Expand Up @@ -152,6 +153,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
{
relayerTokens: [],
minDepositConfirmations: defaultMinDepositConfirmations,
sendingRelaysEnabled: true,
} as unknown as RelayerConfig
);

Expand Down Expand Up @@ -197,11 +199,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {

await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(lastSpyLogIncludes(spy, "Filling v3 deposit")).to.be.true;
expect(multiCallerClient.transactionCount()).to.equal(1); // One transaction, filling the one deposit.

const tx = await multiCallerClient.executeTransactionQueue();
expect(tx.length).to.equal(1); // There should have been exactly one transaction.
expect(lastSpyLogIncludes(spy, "Filled v3 deposit")).to.be.true;
pxrl marked this conversation as resolved.
Show resolved Hide resolved

await Promise.all([spokePoolClient_1.update(), spokePoolClient_2.update(), hubPoolClient.update()]);
let fill = spokePoolClient_2.getFillsForOriginChain(deposit.originChainId).at(-1);
Expand All @@ -215,7 +213,6 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
// Re-run the execution loop and validate that no additional relays are sent.
multiCallerClient.clearTransactionQueue();
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(multiCallerClient.transactionCount()).to.equal(0); // no Transactions to send.
expect(lastSpyLogIncludes(spy, "0 unfilled deposits")).to.be.true;
});

Expand All @@ -224,9 +221,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {

await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(lastSpyLogIncludes(spy, "Filling v3 deposit")).to.be.true;
expect(multiCallerClient.transactionCount()).to.equal(1); // One transaction, filling the one deposit.
await multiCallerClient.executeTxnQueues();
expect(lastSpyLogIncludes(spy, "Filled v3 deposit")).to.be.true;

// The first fill is still pending but if we rerun the relayer loop, it shouldn't try to fill a second time.
await Promise.all([spokePoolClient_1.update(), spokePoolClient_2.update(), hubPoolClient.update()]);
Expand All @@ -248,9 +243,10 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
let unfilledDeposits = await getUnfilledDeposits(spokePoolClients, hubPoolClient);
expect(Object.values(unfilledDeposits).flat().length).to.equal(1);

await relayerInstance.checkForUnfilledDepositsAndFill();
expect(lastSpyLogIncludes(spy, "Filling v3 deposit")).to.be.true;
expect(multiCallerClient.transactionCount()).to.equal(1); // One transaction, filling the one deposit.
// Run the relayer in simulation mode so it doesn't fill the relay.
const simulate = true;
await relayerInstance.checkForUnfilledDepositsAndFill(false, simulate);
expect(spyLogIncludes(spy, -2, "Filled v3 deposit")).is.true;

// Verify that the deposit is still unfilled (relayer didn't execute it).
unfilledDeposits = await getUnfilledDeposits(spokePoolClients, hubPoolClient);
Expand All @@ -264,7 +260,6 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
// Verify that the relayer now sees that the deposit has been filled.
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(lastSpyLogIncludes(spy, "0 unfilled deposits")).to.be.true;
expect(multiCallerClient.transactionCount()).to.equal(0);
});

it("Respects configured relayer routes", async function () {
Expand Down Expand Up @@ -308,8 +303,8 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
});

it("Correctly validates self-relays", async function () {
outputAmount = inputAmount.sub(bnOne);
for (const testDepositor of [depositor, relayer]) {
outputAmount = inputAmount.add(bnOne);
for (const testDepositor of [relayer, depositor]) {
await depositV3(
spokePool_1,
destinationChainId,
Expand All @@ -321,9 +316,10 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
);

await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill();
const expectedTransactions = testDepositor.address === relayer.address ? 1 : 0;
expect(multiCallerClient.transactionCount()).to.equal(expectedTransactions);
await relayerInstance.checkForUnfilledDepositsAndFill(false);
const expectedLog =
testDepositor.address === relayer.address ? "Filled v3 deposit" : "Not relaying unprofitable deposit";
expect(lastSpyLogIncludes(spy, expectedLog)).to.be.true;
}
});

Expand Down Expand Up @@ -361,14 +357,17 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {

await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(multiCallerClient.transactionCount()).to.equal(1);
expect(lastSpyLogIncludes(spy, "Filled v3 deposit")).to.be.true;

await relayerInstance.checkForUnfilledDepositsAndFill();
expect(lastSpyLogIncludes(spy, "0 unfilled deposits")).to.be.true;

await spokePool_2.setCurrentTime(exclusivityDeadline + 1);
await updateAllClients();

// Relayer can unconditionally fill after the exclusivityDeadline.
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(multiCallerClient.transactionCount()).to.equal(2);
expect(lastSpyLogIncludes(spy, "Filled v3 deposit")).to.be.true;
});

it("Ignores deposits older than min deposit confirmation threshold", async function () {
Expand Down Expand Up @@ -423,7 +422,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
// If we reset the timestamp, the relayer will fill the deposit:
hubPoolClient.currentTime = quoteTimestamp;
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(multiCallerClient.transactionCount()).to.equal(1);
expect(lastSpyLogIncludes(spy, "Filled v3 deposit")).to.be.true;
});

it("Ignores deposit with non-empty message", async function () {
Expand Down Expand Up @@ -506,14 +505,9 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
expect(
spy.getCalls().find(({ lastArg }) => lastArg.message.includes("Skipping fill for deposit with message"))
).to.not.be.undefined;
expect(multiCallerClient.transactionCount()).to.equal(0);
} else {
// Now speed up deposit again with a higher fee and a message of 0x. This should be filled.
expect(lastSpyLogIncludes(spy, "Filling v3 deposit")).to.be.true;
expect(multiCallerClient.transactionCount()).to.equal(1); // One transaction, filling the one deposit.

const tx = await multiCallerClient.executeTransactionQueue();
expect(tx.length).to.equal(1); // There should have been exactly one transaction.
expect(lastSpyLogIncludes(spy, "Filled v3 deposit")).to.be.true;

await spokePoolClient_2.update();
let fill = spokePoolClient_2.getFillsForRelayer(relayer.address).at(-1);
Expand All @@ -539,7 +533,6 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {
multiCallerClient.clearTransactionQueue();
await Promise.all([spokePoolClient_1.update(), spokePoolClient_2.update(), hubPoolClient.update()]);
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(multiCallerClient.transactionCount()).to.equal(0); // no Transactions to send.
expect(lastSpyLogIncludes(spy, "0 unfilled deposits")).to.be.true;
});

Expand Down Expand Up @@ -582,8 +575,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () {

await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(lastSpyLogIncludes(spy, "Filling v3 deposit")).to.be.true;
expect(multiCallerClient.transactionCount()).to.equal(1);
expect(lastSpyLogIncludes(spy, "Filled v3 deposit")).to.be.true;
});
});
});
7 changes: 1 addition & 6 deletions test/Relayer.SlowFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,9 @@ describe("Relayer: Initiates slow fill requests", async function () {

await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill();
expect(multiCallerClient.transactionCount()).to.equal(1); // Should be requestV3SlowFill()
expect(spyLogIncludes(spy, -2, "Enqueuing slow fill request.")).to.be.true;
expect(spyLogIncludes(spy, -2, "Requested slow fill for deposit.")).to.be.true;
expect(lastSpyLogIncludes(spy, "Insufficient balance to fill all deposits")).to.be.true;

const tx = await multiCallerClient.executeTransactionQueue();
expect(tx.length).to.equal(1);

// Verify that the slowFill request was received by the destination SpokePoolClient.
await Promise.all([spokePoolClient_1.update(), spokePoolClient_2.update(), hubPoolClient.update()]);
let slowFillRequest = spokePoolClient_2.getSlowFillRequest(deposit);
Expand All @@ -211,7 +207,6 @@ describe("Relayer: Initiates slow fill requests", async function () {
);

await relayerInstance.checkForUnfilledDepositsAndFill();
expect(multiCallerClient.transactionCount()).to.equal(0); // no Transactions to send.
expect(lastSpyLogIncludes(spy, "Insufficient balance to fill all deposits")).to.be.true;
});
});
6 changes: 2 additions & 4 deletions test/Relayer.TokenShortfall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
expect,
getLastBlockTime,
lastSpyLogIncludes,
spyLogIncludes,
setupTokensForWallet,
sinon,
toBN,
Expand Down Expand Up @@ -207,12 +208,9 @@ describe("Relayer: Token balance shortfall", async function () {
await erc20_2.mint(relayer.address, toBN(60).mul(bn10.pow(inputTokenDecimals)));
await updateAllClients();
await relayerInstance.checkForUnfilledDepositsAndFill(noSlowRelays);
expect(spyLogIncludes(spy, -2, "Relayed depositId 0")).to.be.true;
expect(lastSpyLogIncludes(spy, `${await l1Token.symbol()} cumulative shortfall of 190.00`)).to.be.true;
expect(lastSpyLogIncludes(spy, "blocking deposits: 1,2")).to.be.true;

const tx = await multiCallerClient.executeTransactionQueue();
expect(lastSpyLogIncludes(spy, "Relayed depositId 0")).to.be.true;
expect(tx.length).to.equal(1); // There should have been exactly one transaction.
});

it("Produces expected logs based on insufficient multiple token balance", async function () {
Expand Down
Loading