diff --git a/src/relayer/Relayer.ts b/src/relayer/Relayer.ts index a08c50885a..baeee46ace 100644 --- a/src/relayer/Relayer.ts +++ b/src/relayer/Relayer.ts @@ -552,17 +552,18 @@ export class Relayer { const mdcs = this.config.minDepositConfirmations[chainId]; const originSpoke = this.clients.spokePoolClients[chainId]; + // For each deposit confirmation tier (lookback), sum all outstanding commitments back to head. + const { deploymentBlock, latestHeightSearched: head } = originSpoke; let totalCommitment = bnZero; - let toBlock = originSpoke.latestHeightSearched; - // For each deposit confirmation tier (lookback), sum all outstanding commitments back to head. - const limits = mdcs.map(({ usdThreshold, minConfirmations }) => { - const fromBlock = Math.max(toBlock - minConfirmations, originSpoke.deploymentBlock); + const limits = mdcs.map(({ usdThreshold, minConfirmations }, idx) => { + const toBlock = Math.max(head - minConfirmations, deploymentBlock); + const fromBlock = + idx < mdcs.length - 1 ? Math.max(head - mdcs[idx + 1].minConfirmations + 1, 0) : deploymentBlock; const commitment = this.computeOriginChainCommitment(chainId, fromBlock, toBlock); totalCommitment = totalCommitment.add(commitment); const limit = usdThreshold.sub(totalCommitment); - toBlock = fromBlock - 1; // Shuffle the range for the next loop. return { fromBlock, limit }; }); diff --git a/test/Relayer.BasicFill.ts b/test/Relayer.BasicFill.ts index af586b60fc..06f49383e4 100644 --- a/test/Relayer.BasicFill.ts +++ b/test/Relayer.BasicFill.ts @@ -670,7 +670,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () { })); originChainConfirmations.push({ usdThreshold: bnUint256Max, - minConfirmations: originChainConfirmations.length + 1, + minConfirmations: originChainConfirmations.at(-1).minConfirmations + 1, }); relayerInstance = new Relayer( @@ -784,7 +784,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () { expect(originChainLimits.slice(0, -1).every(({ limit }) => limit.lt(bnZero))).to.be.true; }); - it("Ignores deposits with quote times in future", async function () { + it("Ignores deposits that would overcommit the relayer to an origin chain", async function () { await erc20_2.connect(relayer).approve(spokePool_2.address, MAX_SAFE_ALLOWANCE); const outputAmount = toBNWei("0.5"); @@ -794,12 +794,18 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () { // Simple escalating confirmation requirements; cap off with a default upper limit. const fillAmount = outputAmount.mul(tokenPrice).div(fixedPoint); - const originChainConfirmations = [1, 4, 8].map((n) => ({ usdThreshold: fillAmount.mul(n), minConfirmations: n })); + const originChainConfirmations = [0, 1, 4, 8].map((n) => ({ + usdThreshold: fillAmount.mul(n + 1), + minConfirmations: n, + })); originChainConfirmations.push({ usdThreshold: bnUint256Max, - minConfirmations: originChainConfirmations.length + 1, + minConfirmations: originChainConfirmations.at(-1).minConfirmations + 1, }); + const targetIdx = 2; + const targetMDCs = originChainConfirmations.at(targetIdx); + relayerInstance = new Relayer( relayer.address, spyLogger, @@ -827,7 +833,7 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () { } as unknown as RelayerConfig ); - // Make a deposit; the relayer should refuse to fill it until it is no longer overcommitted. + // Make a deposit; the relayer should fill it instantly. const deposit1 = await depositV3( spokePool_1, destinationChainId, @@ -841,78 +847,73 @@ describe("Relayer: Check for Unfilled Deposits and Fill", async function () { let txnReceipts = await relayerInstance.checkForUnfilledDepositsAndFill(); let fills = await (txnReceipts[destinationChainId] ?? Promise.resolve([])); - expect(fills.length).to.equal(0); + expect(fills.length).to.equal(1); + + // Update clients to ensure deposit1's fill is recorded in spoke pool clients + await updateAllClients(); - // And a 2nd deposit; the relayer should defer filling this one after the first deposit. + // Dynamically compute available capacity in target tier after deposit1 is filled + const limitsAfterDeposit1 = relayerInstance.computeOriginChainLimits(originChainId); + const targetFillAmount = limitsAfterDeposit1[targetIdx].limit; + + // And a 2nd deposit that would require at least `targetMDCs.minConfirmations` confirmations. const deposit2 = await depositV3( spokePool_1, destinationChainId, depositor, inputToken, - inputAmount, + targetFillAmount.mul(101).div(100), outputToken, - outputAmount + targetFillAmount ); await updateAllClients(); - // Make an invalid fills to crank the chain forward until the initial deposit has enough confirmations. - while ( - originChainConfirmations[0].minConfirmations > - spokePoolClient_2.latestHeightSearched - deposit1.blockNumber - ) { + // Verify that the relayer does not fill the deposit. + txnReceipts = await relayerInstance.checkForUnfilledDepositsAndFill(); + fills = await (txnReceipts[destinationChainId] ?? Promise.resolve([])); + expect(fills.length).to.equal(0); + + // Make zero-value fills to crank the chain forward, stopping just before deposit2 reaches enough confirmations. + while (spokePoolClient_2.latestHeightSearched - deposit2.blockNumber < targetMDCs.minConfirmations - 1) { await fillV3Relay( spokePool_2, { ...deposit1, depositId: BigNumber.from(randomNumber()), outputAmount: bnZero }, relayer ); await updateAllClients(); - } - const originChainLimits = relayerInstance.computeOriginChainLimits(originChainId); - // Verify that the relayer now fills the latest deposit. - txnReceipts = await relayerInstance.checkForUnfilledDepositsAndFill(); - fills = await (txnReceipts[destinationChainId] ?? Promise.resolve([])); - expect(fills.length).to.equal(1); + // Verify that the relayer continues to refuse to fill. + txnReceipts = await relayerInstance.checkForUnfilledDepositsAndFill(); + fills = await (txnReceipts[destinationChainId] ?? Promise.resolve([])); + expect(fills.length).to.equal(0); + } + // Advance one more block so deposit2 reaches exactly the required confirmations. + await fillV3Relay( + spokePool_2, + { ...deposit1, depositId: BigNumber.from(randomNumber()), outputAmount: bnZero }, + relayer + ); await updateAllClients(); - // Verify the updated limits. Skip the first index because it's already in the past. - relayerInstance - .computeOriginChainLimits(originChainId) - .slice(1) - .forEach(({ limit: newLimit }, idx) => { - const { limit: oldLimit } = originChainLimits.slice(1)[idx]; - expect(newLimit.eq(oldLimit.sub(fillAmount))).to.be.true; - }); - - // Make an invalid fills to crank the chain forward until the 2nd deposit has enough confirmations. - while ( - originChainConfirmations[0].minConfirmations > - spokePoolClient_2.latestHeightSearched - deposit2.blockNumber - ) { - await fillV3Relay( - spokePool_2, - { ...deposit2, depositId: BigNumber.from(randomNumber()), outputAmount: bnZero }, - relayer - ); - await updateAllClients(); - } + // Compute pre-fill limits at the current head (where deposit2 has enough confirmations but hasn't been filled yet). + const originChainLimits = relayerInstance.computeOriginChainLimits(originChainId); - // Verify that the relayer now fills the latest deposit. + // Now fill deposit2. txnReceipts = await relayerInstance.checkForUnfilledDepositsAndFill(); fills = await (txnReceipts[destinationChainId] ?? Promise.resolve([])); expect(fills.length).to.equal(1); + // Compute post-fill limits at the same head value. await updateAllClients(); + const newLimits = relayerInstance.computeOriginChainLimits(originChainId); - // Verify the updated limits. Skip the first index because it's already in the past. - relayerInstance - .computeOriginChainLimits(originChainId) - .slice(1) - .forEach(({ limit: newLimit }, idx) => { - const { limit: oldLimit } = originChainLimits.slice(1)[idx]; - expect(newLimit.eq(oldLimit.sub(fillAmount.mul(2)))).to.be.true; - }); + // Verify that tier targetIdx and all subsequent tiers have their limits reduced by targetFillAmount. + originChainLimits.forEach(({ limit: oldLimit }, idx) => { + const newLimit = newLimits[idx].limit; + const expectedLimit = idx < targetIdx ? oldLimit : oldLimit.sub(targetFillAmount); + expect(newLimit.eq(expectedLimit)).to.be.true; + }); }); it("Ignores deposits with quote times in future", async function () {