{
+ fs.rmSync(path.join(directory, 'scripts/fetchedAddressData.json'), { force: true })
+ fs.rmSync(path.join(directory, 'issue_body.md'), { force: true })
+ fs.writeFileSync(path.join(directory, 'all_checks_passed.txt'), 'false')
+ try {
+ const result = await collect()
+ if (!result) throw new Error('Failed to fetch addresses')
+ const passed = writeAddressCheckReport({
+ status: 'complete',
+ timeLastChecked: Math.floor(Date.now() / 1000),
+ ...result,
+ }, directory)
+ console.log('allChecksPassed:', passed)
+ console.log('Wrote fresh report to scripts/fetchedAddressData.json')
+ return passed ? 0 : 1
+ } catch (error) {
+ console.error('Address checks could not complete:', error instanceof Error ? error.message : error)
+ return 2
}
- const timeLastChecked = Math.floor(Date.now() / 1000) // get current time in Unix format
- console.log('writing report to scripts/fetchedAddressData.json')
- fs.writeFileSync(
- 'scripts/fetchedAddressData.json',
- JSON.stringify({ timeLastChecked, addressesData, addressChecks }, null, 2)
- )
}
-runAddressCheck()
+if (import.meta.main) process.exitCode = await runAddressCheck()
diff --git a/scripts/withAddressChecks.ts b/scripts/withAddressChecks.ts
new file mode 100644
index 0000000000..a7dd926da9
--- /dev/null
+++ b/scripts/withAddressChecks.ts
@@ -0,0 +1,47 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { spawnSync } from 'node:child_process'
+import { didAddressChecksPass, isCompleteAddressCheckReport } from './addressCheckReport'
+
+type RunCommand = (script: string, directory: string) => number
+const runCommand: RunCommand = (script, directory) => {
+ const result = spawnSync('bun', ['run', script], { cwd: directory, stdio: 'inherit' })
+ if (result.error) console.error(result.error.message)
+ return result.status ?? 2
+}
+
+export function runWithAddressChecks(script: 'build:site' | 'start-no-check', run: RunCommand = runCommand, directory = process.cwd()): number {
+ const reportPath = path.join(directory, 'scripts/fetchedAddressData.json')
+ // Even an early crash in the checker must not leave an old green snapshot.
+ fs.rmSync(reportPath, { force: true })
+ const startedAt = Math.floor(Date.now() / 1000)
+ const result = run('runAddressCheck', directory)
+ if (result !== 0 && result !== 1) {
+ console.error(`Address checks could not complete. ${script} stopped.`)
+ return 2
+ }
+ try {
+ const report: unknown = JSON.parse(fs.readFileSync(reportPath, 'utf8'))
+ if (!isCompleteAddressCheckReport(report) || report.timeLastChecked < startedAt) {
+ throw new Error('Missing, stale, or incomplete address-check report')
+ }
+ if ((result === 0) !== didAddressChecksPass(report.addressChecks)) {
+ throw new Error('Address-check exit status disagrees with its report')
+ }
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : error)
+ return 2
+ }
+ if (result === 1) console.warn('Address mismatches found. Proceeding with the fresh warning report.')
+ return run(script, directory)
+}
+
+if (import.meta.main) {
+ const script = process.argv[2]
+ if (script !== 'build:site' && script !== 'start-no-check') {
+ console.error('Choose build:site or start-no-check')
+ process.exitCode = 2
+ } else {
+ process.exitCode = runWithAddressChecks(script)
+ }
+}
diff --git a/src/components/AddressCheck.tsx b/src/components/AddressCheck.tsx
index 2bf1485f97..e359afdea5 100644
--- a/src/components/AddressCheck.tsx
+++ b/src/components/AddressCheck.tsx
@@ -57,8 +57,8 @@ function AddressCheck({ contractType }: AddressCheckProps) {
⚠️
- Some Addresses on this page don't match on-chain data. Check the
- console for details.
+ Address validation on this page found mismatches. Last checked on:{' '}
+ {lastTimeCheckedUTC} UTC. Check the console for details.
)}
diff --git a/src/css/AddressCheck.module.css b/src/css/AddressCheck.module.css
index d479373924..ee1675fa43 100644
--- a/src/css/AddressCheck.module.css
+++ b/src/css/AddressCheck.module.css
@@ -1,7 +1,6 @@
.addressCheckWrapper {
width: 100%;
padding-bottom: 1rem;
- height: 6rem;
}
.loading {
diff --git a/src/ethereum/v3Calls.ts b/src/ethereum/v3Calls.ts
index 5cfcef6770..32c1455a39 100644
--- a/src/ethereum/v3Calls.ts
+++ b/src/ethereum/v3Calls.ts
@@ -1,4 +1,7 @@
-import { getContract, PublicClient, Address, getAddress } from 'viem'
+import {
+ getContract, PublicClient, Address, getAddress, BaseError,
+ ContractFunctionRevertedError, ContractFunctionZeroDataError,
+} from 'viem'
import {
v3ProtocolAddressProviderABI,
v3ReleaseRegistryABI,
@@ -10,6 +13,15 @@ import {
} from './ABIs'
import { V3ReleaseDataMap } from './types'
+function allowMissingContractFunction(error: unknown) {
+ // A missing/reverting legacy getter is a completed on-chain observation.
+ // Transport errors and other unavailable reads must abort validation.
+ const missingFunction = error instanceof BaseError && error.walk(cause =>
+ cause instanceof ContractFunctionRevertedError || cause instanceof ContractFunctionZeroDataError
+ )
+ if (!missingFunction) throw error
+}
+
const useFallback = (contractName: string, fallback: string): Address => {
console.warn(
`RPC call failed for ${contractName}. Using fallback address: ${fallback}`
@@ -35,19 +47,23 @@ export const getProtocolContractAddresses = async (
v3ReportTrigger,
v3RoleManagerFactory,
] = await Promise.all([
- contract.read.getRouter().catch(() => {
+ contract.read.getRouter().catch(error => {
+ allowMissingContractFunction(error)
console.warn('v3Router not found')
return undefined
}),
- contract.read.getAprOracle().catch(() => {
+ contract.read.getAprOracle().catch(error => {
+ allowMissingContractFunction(error)
console.warn('v3AprOracle not found')
return undefined
}),
- contract.read.getCommonReportTrigger().catch(() => {
+ contract.read.getCommonReportTrigger().catch(error => {
+ allowMissingContractFunction(error)
console.warn('v3ReportTrigger not found')
return undefined
}),
- contract.read.getRoleManagerFactory().catch(() => {
+ contract.read.getRoleManagerFactory().catch(error => {
+ allowMissingContractFunction(error)
console.warn('v3RoleManagerFactory not found')
return undefined
}),
@@ -99,7 +115,8 @@ export const readReleaseRegistryAll = async (
const releaseNumber = await factoryContract.read.apiVersion()
const vaultAddress = await factoryContract.read
.vault_original()
- .catch(async () => {
+ .catch(async error => {
+ allowMissingContractFunction(error)
// Re-initialize factoryContract with v3VaultFactoryBlueprintABI if fallback occurs
const factoryContractWithBlueprint = getContract({
address: factoryAddress,
@@ -138,15 +155,18 @@ export const readYearnRoleManager = async (
] = await Promise.all([
contract.read.getBrain(),
contract.read.getDaddy(),
- contract.read.getAccountant().catch(() => {
+ contract.read.getAccountant().catch(error => {
+ allowMissingContractFunction(error)
console.warn('yearnAccountant not found')
return undefined
}),
- contract.read.getDebtAllocator().catch(() => {
+ contract.read.getDebtAllocator().catch(error => {
+ allowMissingContractFunction(error)
console.warn('yearnDebtAllocator not found')
return undefined
}),
- contract.read.getRegistry().catch(() => {
+ contract.read.getRegistry().catch(error => {
+ allowMissingContractFunction(error)
console.warn('yearnRegistry not found')
return undefined
}),
@@ -172,7 +192,8 @@ export const getAuctionFactoryFromRegistry = async (
client: publicClient,
})
console.log('Fetching auction factory from auction registry...')
- const auctionFactory = await contract.read.getLatestFactory().catch(() => {
+ const auctionFactory = await contract.read.getLatestFactory().catch(error => {
+ allowMissingContractFunction(error)
console.warn('auctionFactory not found in auction registry')
return undefined
})
@@ -190,7 +211,8 @@ export const getFeeRecipientFromAccountant = async (
client: publicClient,
})
console.log('Fetching fee recipient from accountant...')
- const feeRecipient = await contract.read.feeRecipient().catch(() => {
+ const feeRecipient = await contract.read.feeRecipient().catch(error => {
+ allowMissingContractFunction(error)
console.warn('feeRecipient not found in accountant')
return undefined
})
diff --git a/src/ethereum/v3Checks.ts b/src/ethereum/v3Checks.ts
index 7f0bfc1d76..83088cc15e 100644
--- a/src/ethereum/v3Checks.ts
+++ b/src/ethereum/v3Checks.ts
@@ -249,10 +249,23 @@ export const fetchAndCheckFromReleaseRegistry = async (
'Latest Release in Constants file does not match Release Registry Contract.'
)
hasLatestRelease = false
- const latestRelease = `latest V3 Release: ${addresses.latestRelease}`
+ checkFlag = false
+ const latestRelease = `latest V3 Release: ${addresses.latestRelease} (expected ${constants.releaseRegistry.latestRelease})`
failedChecks.push(latestRelease)
}
- const checks = { hasLatestRelease }
+ const checks: Record = { hasLatestRelease }
+
+ // Every documented release needs on-chain evidence, including a release
+ // which is present in constants but not yet registered on this chain.
+ for (const releaseNumber of Object.keys(constants.releaseRegistry)) {
+ if (releaseNumber === 'latestRelease') continue
+ const hasRelease = typeof addresses[releaseNumber] === 'object'
+ checks[`has${releaseNumber.replace(/\./g, '')}Release`] = hasRelease
+ if (!hasRelease) {
+ checkFlag = false
+ failedChecks.push(`V3 Release ${releaseNumber} is missing from the release registry`)
+ }
+ }
for (const releaseNumber in addresses) {
if (releaseNumber === 'latestRelease') continue // Skip the latestRelease key
@@ -260,6 +273,7 @@ export const fetchAndCheckFromReleaseRegistry = async (
const constantRelease = constants.releaseRegistry[releaseNumber]
if (!constantRelease) {
console.warn(`Release ${releaseNumber} is missing in constants.`)
+ checkFlag = false
failedChecks.push(`V3 Release ${releaseNumber} is missing`)
continue
}
diff --git a/src/ethereum/veYfiChecks.ts b/src/ethereum/veYfiChecks.ts
index de57c6cd31..e6e830599c 100644
--- a/src/ethereum/veYfiChecks.ts
+++ b/src/ethereum/veYfiChecks.ts
@@ -17,10 +17,12 @@ export const veYfiChecks = async (
console.log('validating veYFI Gauge addresses...')
if (numberOfGauges < 1) {
+ checkFlag = false
console.warn('No gauges found')
failedChecks.push('No gauges found')
}
if (numberOfGauges !== constants.veYfiGauges.length) {
+ checkFlag = false
console.warn('Number of gauges does not match number of fallback addresses')
failedChecks.push(
'Number of gauges does not match number of fallback addresses'
@@ -31,8 +33,17 @@ export const veYfiChecks = async (
// Validate each entry in gaugeAddresses against the corresponding address in constants.veYfiGauges
for (const [index, address] of gaugeAddresses.entries()) {
- const expectedAddress = constants.veYfiGauges[index].address
- const expectedName = constants.veYfiGauges[index].name
+ const expectedGauge = constants.veYfiGauges[index]
+ if (!expectedGauge) {
+ checkFlag = false
+ const name = `Gauge ${index}`
+ failedChecks.push(`veYFI ${name} (${address}) is missing in constants`)
+ veYfiGaugeChecks[name] = false
+ veYfiGaugeAddresses[name] = address
+ continue
+ }
+ const expectedAddress = expectedGauge.address
+ const expectedName = expectedGauge.name
console.log(
`Validating ${expectedName} (${expectedAddress}) against ${address}...`
)