diff --git a/packages/financial-templates-lib/src/clients/InsuredBridgeL2Client.ts b/packages/financial-templates-lib/src/clients/InsuredBridgeL2Client.ts index 30d55bfb77..90a4a5cfa3 100644 --- a/packages/financial-templates-lib/src/clients/InsuredBridgeL2Client.ts +++ b/packages/financial-templates-lib/src/clients/InsuredBridgeL2Client.ts @@ -36,7 +36,8 @@ export class InsuredBridgeL2Client { readonly chainId: number = 0, readonly startingBlockNumber: number = 0, readonly endingBlockNumber: number | null = null, - readonly redundantL2Web3s: Web3[] = [l2Web3] + readonly redundantL2Web3s: Web3[] = [l2Web3], + readonly maxBlockTimestampVariance: number = 300 // 5 minutes ) { this.bridgeDepositBox = (new l2Web3.eth.Contract( getAbi("BridgeDepositBox"), @@ -65,7 +66,7 @@ export class InsuredBridgeL2Client { // Define a config to bound the queries by. const blockSearchConfig = { fromBlock: this.firstBlockToSearch, - toBlock: this.endingBlockNumber || (await this.l2Web3.eth.getBlockNumber()), + toBlock: this.endingBlockNumber || (await this.getLatestBlockNumber()), }; if (blockSearchConfig.fromBlock > blockSearchConfig.toBlock) { this.logger.debug({ @@ -105,6 +106,30 @@ export class InsuredBridgeL2Client { }); } + async getLatestBlockNumber(): Promise { + const blocks = await Promise.all(this.redundantL2Web3s.map((web3) => web3.eth.getBlock("latest"))); + + // Sorts from smallest to largest. + const timestamps = blocks.map((block) => Number(block.timestamp)); + + // Throw if one of the blocks is too far back in time. This is used to detect providers that are hanging or blocked. + if (Math.max(...timestamps) - Math.min(...timestamps) > this.maxBlockTimestampVariance) { + const error = new Error(`Timestamps for chainId ${this.chainId} differ by too much time.`); + + this.logger.error({ + at: "InsuredBridgeL2Client", + message: "Timestamps from providers differ by too much time ⏰", + chainId: this.chainId, + error, + }); + throw error; + } + + const blockNumbers = blocks.map((block) => block.number); + + return Math.min(...blockNumbers); + } + async getBridgeDepositBoxEvents(eventSearchOptions: EventSearchOptions, eventName: string): Promise { // Note: the primary l2Web3 is not used here because the main l2Web3 usually uses a combination of the list of // redundant providers. Including both would mean calling the same provider(s) twice. diff --git a/packages/financial-templates-lib/test/clients/InsuredBridgeL2Client.js b/packages/financial-templates-lib/test/clients/InsuredBridgeL2Client.js index 668cc18a6b..1998a9bc12 100644 --- a/packages/financial-templates-lib/test/clients/InsuredBridgeL2Client.js +++ b/packages/financial-templates-lib/test/clients/InsuredBridgeL2Client.js @@ -11,7 +11,7 @@ const sinon = require("sinon"); // Client to test const { InsuredBridgeL2Client } = require("../../dist/clients/InsuredBridgeL2Client"); -const { ZERO_ADDRESS } = require("@uma/common"); +const { ZERO_ADDRESS, advanceBlockAndSetTime } = require("@uma/common"); // Helper contracts const chainId = 10; @@ -167,15 +167,17 @@ describe("InsuredBridgeL2Client", () => { transports: [new SpyTransport({ level: "debug" }, { spy: spy })], }); + const ganacheWeb3 = new Web3(ganache.provider()); + // Construct new client where we pass in a fallback L2 web3. - const clientWithFallbackWeb3s = new InsuredBridgeL2Client( + let clientWithFallbackWeb3s = new InsuredBridgeL2Client( spyLogger, web3, depositBox.options.address, chainId, 0, null, - [web3, new Web3(ganache.provider())] // Ganache provider will be different from hardhat provider that is already + [web3, ganacheWeb3] // Ganache provider will be different from hardhat provider that is already // connected to the BridgeDepositBox. ); @@ -211,6 +213,17 @@ describe("InsuredBridgeL2Client", () => { assert.equal(spy.getCall(-1).lastArg.eventName, "FundsDeposited"); } + clientWithFallbackWeb3s = new InsuredBridgeL2Client( + spyLogger, + web3, + depositBox.options.address, + chainId, + 0, + (await web3.eth.getBlock("latest")).number, // Ensure that we query to the end. + [web3, ganacheWeb3] // Ganache provider will be different from hardhat provider that is already + // connected to the BridgeDepositBox. + ); + // Update will throw an error. try { await clientWithFallbackWeb3s.update(); @@ -219,5 +232,28 @@ describe("InsuredBridgeL2Client", () => { assert.equal(lastSpyLogLevel(spy), "error"); assert.isTrue(lastSpyLogIncludes(spy, "L2 RPC endpoint state disagreement")); } + + // Set the ganache time past the latest block time to generate a different error. + await advanceBlockAndSetTime(ganacheWeb3, Number((await web3.eth.getBlock("latest")).timestamp + 1000)); + + clientWithFallbackWeb3s = new InsuredBridgeL2Client( + spyLogger, + web3, + depositBox.options.address, + chainId, + 0, + null, // Allow the client to determine how far we query. + [web3, ganacheWeb3] // Ganache provider will be different from hardhat provider that is already + // connected to the BridgeDepositBox. + ); + + // Update will throw an error. + try { + await clientWithFallbackWeb3s.update(); + assert.isTrue(false); + } catch (e) { + assert.equal(lastSpyLogLevel(spy), "error"); + assert.isTrue(lastSpyLogIncludes(spy, "differ by too much time.")); + } }); });