From 0a4a365f5ac333a1e7ce701a457478f42d3f5782 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 07:14:15 +0000 Subject: [PATCH] Soft-fail upgrade history when Blockscout deployment lookup fails. If getDeploymentBlocks throws, still return proxy/implementation data with wasUpgraded=null so review generation can continue without inventing a "not upgraded" result. Co-authored-by: gosuto.eth --- src/app.ts | 37 ++++++++++++--- .../upgradeableContractsSoftFail.test.ts | 47 +++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 test/typescript/upgradeableContractsSoftFail.test.ts diff --git a/src/app.ts b/src/app.ts index 6d45abb5..b810ccff 100644 --- a/src/app.ts +++ b/src/app.ts @@ -126,11 +126,12 @@ class RateProviderDataService { /** * Fetches upgradeable contracts information. Includes the rate provider if upgradeable. * @returns An array of upgradeable contracts information. + * `wasUpgraded` is `null` when deployment/upgrade history could not be verified. */ public async getUpgradeableContracts(): Promise< { address: Address - wasUpgraded: boolean + wasUpgraded: boolean | null implementation: Address }[] > { @@ -147,9 +148,29 @@ class RateProviderDataService { // filter out the contracts that are not proxies const filteredProxiesList = proxiesWithRateProvider.filter((p) => p.Proxy === '1') - const proxiesWithRateProviderDeploymentInfo = await this.getDeploymentBlocks( - filteredProxiesList.map((p) => p.address), - ) + let proxiesWithRateProviderDeploymentInfo: { address: Address; deploymentTxHash: Hex }[] + try { + proxiesWithRateProviderDeploymentInfo = await this.getDeploymentBlocks( + filteredProxiesList.map((p) => p.address), + ) + } catch (error) { + // Explorer deployment lookup can fail (e.g. Blockscout HTTP 500 on Base). + // Reviews only need proxy + implementation; leave wasUpgraded unknown. + console.warn( + `Failed to determine upgrade history for ${this.chain.name}; continuing with proxy list only:`, + error instanceof Error ? error.message : error, + ) + return filteredProxiesList.map((p) => { + if (!p.Implementation) { + throw new Error(`Implementation is undefined for contract at address ${p.address}`) + } + return { + address: p.address, + wasUpgraded: null, + implementation: p.Implementation, + } + }) + } const publicClient = createPublicClient({ chain: this.chain, @@ -205,11 +226,15 @@ class RateProviderDataService { /** * Checks if the rate provider is upgradeable. - * @returns True if the rate provider is upgradeable, false otherwise. + * @returns True if an Upgraded event was positively observed. + * @throws If upgrade history could not be verified for any proxy. */ public async getIsRateProviderUpgradeable(): Promise { const upgradeableContracts = await this.getUpgradeableContracts() - return upgradeableContracts.some((c) => c.wasUpgraded) + if (upgradeableContracts.some((c) => c.wasUpgraded === null)) { + throw new Error(`Upgrade history could not be verified for one or more proxies on ${this.chain.name}`) + } + return upgradeableContracts.some((c) => c.wasUpgraded === true) } /** diff --git a/test/typescript/upgradeableContractsSoftFail.test.ts b/test/typescript/upgradeableContractsSoftFail.test.ts new file mode 100644 index 00000000..9f74c1e2 --- /dev/null +++ b/test/typescript/upgradeableContractsSoftFail.test.ts @@ -0,0 +1,47 @@ +import { Address } from 'viem' +import { base } from 'viem/chains' +import RateProviderDataService from '../../src/app' + +describe('RateProviderDataService.getUpgradeableContracts soft-fail', () => { + const rateProvider = '0x586d1D28D5F8aeD81dA8AEf7016c4108f7A4ce51' as Address + const implementation = '0xb3201cde011873f604bf3f9be6bf3570ecc29b23' as Address + + beforeEach(() => { + process.env.TENDERLY_ACCOUNT_SLUG = 'test-account' + process.env.TENDERLY_PROJECT_SLUG = 'test-project' + process.env.TENDERLY_API_ACCESS_KEY = 'test-key' + process.env.ETHERSCAN_API_KEY = 'test-etherscan-key' + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('returns proxies with wasUpgraded=null when deployment lookup fails', async () => { + const service = new RateProviderDataService(rateProvider, base) + service.accessList = { + accessList: [{ address: rateProvider, storageKeys: [] }], + gasUsed: 0n, + } + + jest.spyOn(service, 'getContractInfo').mockResolvedValue([ + { + address: rateProvider, + Proxy: '1', + ContractName: 'HilbertBTCRateProvider', + ABI: '[]', + Implementation: implementation, + }, + ]) + jest.spyOn(service, 'getDeploymentBlocks').mockRejectedValue(new Error('Blockscout 500')) + jest.spyOn(console, 'warn').mockImplementation(() => undefined) + + await expect(service.getUpgradeableContracts()).resolves.toEqual([ + { + address: rateProvider, + wasUpgraded: null, + implementation, + }, + ]) + }) +})