UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

349 lines 15.3 kB
import { CrossCollateralRouter__factory, IERC4626__factory, IXERC20Lockbox__factory, Ownable__factory, ProxyAdmin__factory, } from '@hyperlane-xyz/core'; import { assert, bytes32ToAddress, concurrentMap, deepCopy, diffObjMerge, eqAddress, isAddressEvm, isEVMLike, keepOnlyDiffObjects, normalizeAddressEvm, objFilter, objMap, promiseObjAll, } from '@hyperlane-xyz/utils'; import { isProxy, proxyAdmin } from '../deploy/proxy.js'; import { resolveRouterMapConfig } from '../router/types.js'; import { verifyScale } from '../utils/decimals.js'; import { EvmWarpRouteReader } from './EvmWarpRouteReader.js'; import { TokenType } from './config.js'; import { expandVirtualWarpDeployConfig, expandWarpDeployConfig, getRouterAddressesFromWarpCoreConfig, normalizeWarpDeployConfigForCheck, transformConfigToCheck, } from './configUtils.js'; import { derivedHookAddress, derivedIsmAddress, isCollateralTokenConfig, isCrossCollateralTokenConfig, isXERC20TokenConfig, } from './types.js'; export const WARP_ROUTE_CHECK_TYPE = 'ConfigMismatch'; export const WARP_ROUTE_CHECK_SCALE_TYPE = 'ScaleMismatch'; async function getWarpRouteConfigsByCore({ multiProvider, warpCoreConfig, }) { const addresses = Object.fromEntries(warpCoreConfig.tokens.map(({ chainName, addressOrDenom }) => { assert(addressOrDenom, `Missing addressOrDenom for ${chainName}`); return [chainName, addressOrDenom]; })); return promiseObjAll(objMap(addresses, async (chain, address) => { const protocol = multiProvider.getProtocol(chain); assert(isEVMLike(protocol), `Warp route core config fetch only supports EVM chains, got ${protocol} for ${chain}`); return new EvmWarpRouteReader(multiProvider, chain).deriveWarpRouteConfig(address); })); } export async function checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig, warpDeployConfig, }) { const knownWarpCoreTokens = warpCoreConfig.tokens.filter((token) => multiProvider.tryGetProtocol(token.chainName) !== null); const evmWarpCoreConfig = { ...warpCoreConfig, tokens: knownWarpCoreTokens.filter((token) => isEVMLike(multiProvider.getProtocol(token.chainName))), }; assert(evmWarpCoreConfig.tokens.length > 0, 'Warp route check requires at least one EVM chain in the selected route config'); const deployedRoutersAddresses = objFilter(getRouterAddressesFromWarpCoreConfig(warpCoreConfig), (chain, _address) => multiProvider.tryGetProtocol(chain) !== null); const onChainWarpConfig = await getWarpRouteConfigsByCore({ multiProvider, warpCoreConfig: evmWarpCoreConfig, }); const expandedOnChainWarpConfig = await expandVirtualWarpDeployConfig({ multiProvider, onChainWarpConfig, deployedRoutersAddresses, }); const expandedWarpDeployConfig = await expandWarpDeployConfig({ multiProvider, warpDeployConfig, deployedRoutersAddresses, expandedOnChainWarpConfig, validateScale: false, }); const normalizedWarpDeployConfig = normalizeWarpDeployConfigForCheck({ multiProvider, warpDeployConfig: expandedWarpDeployConfig, }); const evmExpandedWarpDeployConfig = objFilter(normalizedWarpDeployConfig, (chain, _config) => isEVMLike(multiProvider.getProtocol(chain))); const rawDiff = buildWarpRouteDiff({ onChainWarpConfig: expandedOnChainWarpConfig, warpRouteConfig: evmExpandedWarpDeployConfig, }); await addOwnerOverrideDiffs({ multiProvider, diff: rawDiff, warpRouteConfig: evmExpandedWarpDeployConfig, }); const diff = keepOnlyDiffObjects(rawDiff); // CAST: keepOnlyDiffObjects returns `any`; rawDiff is constructed as a chain-keyed ObjectDiff map const diffViolations = flattenWarpRouteCheckDiff(diff); const scaleViolations = await getScaleViolations({ multiProvider, warpRouteConfig: normalizedWarpDeployConfig, }); return { diff, isValid: diffViolations.length === 0 && scaleViolations.length === 0, scaleViolations, violations: [...diffViolations, ...scaleViolations], }; } function buildWarpRouteDiff({ warpRouteConfig, onChainWarpConfig, }) { return Object.keys(warpRouteConfig).reduce((acc, chain) => { const expectedDeployedConfig = deepCopy(warpRouteConfig[chain]); const currentDeployedConfig = deepCopy(onChainWarpConfig[chain]); if (!currentDeployedConfig) { acc[chain] = { route: { actual: 'missing', expected: 'present', }, }; return acc; } if (typeof expectedDeployedConfig.hook === 'string') { currentDeployedConfig.hook = derivedHookAddress(currentDeployedConfig); } if (typeof expectedDeployedConfig.interchainSecurityModule === 'string') { currentDeployedConfig.interchainSecurityModule = derivedIsmAddress(currentDeployedConfig); } if (!expectedDeployedConfig.contractVersion) { currentDeployedConfig.contractVersion = undefined; } if (!expectedDeployedConfig.proxyAdmin?.address) { currentDeployedConfig.proxyAdmin = currentDeployedConfig.proxyAdmin ? { ...currentDeployedConfig.proxyAdmin, address: undefined } : undefined; } const { mergedObject, isInvalid } = diffObjMerge(transformConfigToCheck(currentDeployedConfig), transformConfigToCheck(expectedDeployedConfig)); if (isInvalid) { acc[chain] = mergedObject; } return acc; }, {}); } async function addOwnerOverrideDiffs({ multiProvider, diff, warpRouteConfig, }) { for (const [chain, config] of Object.entries(warpRouteConfig)) { const ownerOverrides = config.ownerOverrides; if (!ownerOverrides || !isEVMLike(multiProvider.getProtocol(chain))) { continue; } const provider = multiProvider.getProvider(chain); if (ownerOverrides.collateralToken) { const collateralToken = await getCollateralOwnable(config, provider); if (collateralToken) { const actualOwner = await collateralToken.owner(); if (!eqAddress(actualOwner, ownerOverrides.collateralToken)) { addNestedDiff(diff, chain, ['ownerOverrides', 'collateralToken'], { actual: actualOwner, expected: ownerOverrides.collateralToken, }); } } } if (ownerOverrides.collateralProxyAdmin) { const collateralTokenAddress = await getCollateralTokenAddress(config, provider); if (collateralTokenAddress && (await isProxy(provider, collateralTokenAddress))) { const collateralProxyAdminAddress = await proxyAdmin(provider, collateralTokenAddress); const actualOwner = await ProxyAdmin__factory.connect(collateralProxyAdminAddress, provider).owner(); if (!eqAddress(actualOwner, ownerOverrides.collateralProxyAdmin)) { addNestedDiff(diff, chain, ['ownerOverrides', 'collateralProxyAdmin'], { actual: actualOwner, expected: ownerOverrides.collateralProxyAdmin, }); } } } } } async function getCollateralTokenAddress(config, provider) { if (isXERC20TokenConfig(config)) { if (config.type === TokenType.XERC20Lockbox) { return IXERC20Lockbox__factory.connect(config.token, provider).callStatic['XERC20()'](); } return config.token; } if (isCollateralTokenConfig(config) || isCrossCollateralTokenConfig(config)) { if (config.type === TokenType.collateralVault || config.type === TokenType.collateralVaultRebase) { return IERC4626__factory.connect(config.token, provider).asset(); } return config.token; } return undefined; } async function getCollateralOwnable(config, provider) { // Preserve legacy checker behavior: only the XERC20 collateral side is // assumed to expose Ownable for explicit collateralToken override checks. if (!isXERC20TokenConfig(config)) { return undefined; } const collateralTokenAddress = await getCollateralTokenAddress(config, provider); return collateralTokenAddress ? Ownable__factory.connect(collateralTokenAddress, provider) : undefined; } function addNestedDiff(diff, chain, path, value) { if (!diff[chain]) { diff[chain] = {}; } let cursor = diff[chain]; assertObjectDiffMap(cursor, `Unexpected leaf diff for ${chain}; refusing to overwrite it`); for (const key of path.slice(0, -1)) { if (!cursor[key]) { cursor[key] = {}; } const nextCursor = cursor[key]; assertObjectDiffMap(nextCursor, `Unexpected leaf diff for ${chain}.${key}; refusing to overwrite it`); cursor = nextCursor; } cursor[path[path.length - 1]] = value; } function flattenWarpRouteCheckDiff(diff) { return Object.entries(diff).flatMap(([chain, chainDiff]) => flattenDiffNode(chain, chainDiff, [])); } function flattenDiffNode(chain, value, path) { if (!value) { return []; } if (Array.isArray(value)) { return value.flatMap((item, index) => flattenDiffNode(chain, item, [...path, index.toString()])); } if (typeof value === 'object') { const objectValue = value; // CAST: runtime guard above narrows to object; Object.entries needs an indexable shape const childViolations = Object.entries(objectValue) .filter(([key]) => key !== 'actual' && key !== 'expected') .flatMap(([key, child]) => flattenDiffNode(chain, child, [...path, key])); if (childViolations.length > 0) { return childViolations; } if (isObjectDiffLeaf(value)) { return [ { actual: stringifyViolationValue(value.actual), chain, expected: stringifyViolationValue(value.expected), name: path.join('.'), type: WARP_ROUTE_CHECK_TYPE, }, ]; } return []; } return []; } function collectConfiguredCrossCollateralRouters({ multiProvider, warpRouteConfig, }) { const routerRefs = new Map(); for (const config of Object.values(warpRouteConfig)) { if (!isCrossCollateralTokenConfig(config) || !config.crossCollateralRouters) { continue; } const crossCollateralRouters = resolveRouterMapConfig(multiProvider, config.crossCollateralRouters); for (const [domain, routers] of Object.entries(crossCollateralRouters)) { const chain = multiProvider.tryGetChainName(Number(domain)); if (!chain || !isEVMLike(multiProvider.getProtocol(chain))) { continue; } for (const routerId of routers) { const routerAddress = normalizeAddressEvm(isAddressEvm(routerId) ? routerId : bytes32ToAddress(routerId)); const metadataKey = `${chain}:${routerAddress.toLowerCase()}`; routerRefs.set(metadataKey, { chain, metadataKey, routerAddress, routerId, }); } } } return [...routerRefs.values()]; } async function fetchConfiguredCrossCollateralRouterMetadata({ multiProvider, readerByChain, routerRef, }) { const { chain, metadataKey, routerAddress, routerId } = routerRef; const reader = readerByChain.get(chain) ?? new EvmWarpRouteReader(multiProvider, chain); readerByChain.set(chain, reader); try { const crossCollateralRouter = CrossCollateralRouter__factory.connect(routerAddress, multiProvider.getProvider(chain)); const [wrappedTokenAddress, scale] = await Promise.all([ crossCollateralRouter.wrappedToken(), reader.fetchScale(routerAddress), ]); const metadata = await reader.fetchERC20Metadata(wrappedTokenAddress); return [metadataKey, { ...metadata, scale }]; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to derive configured crossCollateral router ${routerId} on ${chain}: ${message}`); } } async function buildScaleValidationMetadataMap({ multiProvider, warpRouteConfig, }) { const metadataByKey = new Map(Object.entries(warpRouteConfig).map(([chain, config]) => [ chain, { decimals: config.decimals, name: config.name ?? 'unknown', scale: config.scale, symbol: config.symbol ?? 'unknown', }, ])); const readerByChain = new Map(); const configuredRouters = collectConfiguredCrossCollateralRouters({ multiProvider, warpRouteConfig, }); const configuredRouterMetadata = await concurrentMap(6, configuredRouters, async (routerRef) => fetchConfiguredCrossCollateralRouterMetadata({ multiProvider, readerByChain, routerRef, })); for (const [metadataKey, metadata] of configuredRouterMetadata) { metadataByKey.set(metadataKey, metadata); } return metadataByKey; } export async function getScaleViolations({ multiProvider, warpRouteConfig, }) { const scaleValidationMetadata = await buildScaleValidationMetadataMap({ multiProvider, warpRouteConfig, }); if (verifyScale(scaleValidationMetadata)) { return []; } return [ { actual: 'invalid-or-missing', chain: 'route', expected: 'consistent-with-decimals', name: 'scale', type: WARP_ROUTE_CHECK_SCALE_TYPE, }, ]; } function stringifyViolationValue(value) { if (typeof value === 'bigint') { return value.toString(); } if (value === undefined) { return ''; } if (typeof value === 'string') { return value; } if (value === null) { return 'null'; } if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'symbol' || typeof value === 'function') { return value.toString(); } if (Array.isArray(value)) { return `[${value.map(stringifyViolationValue).join(',')}]`; } return `{${Object.entries(value) .map(([key, child]) => `${key}:${stringifyViolationValue(child)}`) .join(',')}}`; } function isObjectDiffLeaf(value) { return (!!value && typeof value === 'object' && !Array.isArray(value) && 'actual' in value && 'expected' in value); } function isObjectDiffMap(value) { return (!!value && typeof value === 'object' && !Array.isArray(value) && !isObjectDiffLeaf(value)); } function assertObjectDiffMap(value, message) { assert(isObjectDiffMap(value), message); } //# sourceMappingURL=warpCheck.js.map