UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

233 lines 11.5 kB
import { BigNumber } from 'ethers'; import { ERC20__factory, IERC4626__factory, IXERC20Lockbox__factory, Ownable__factory, ProxyAdmin__factory, } from '@hyperlane-xyz/core'; import { eqAddress, objMap } from '@hyperlane-xyz/utils'; import { filterOwnableContracts } from '../contracts/contracts.js'; import { isProxy, proxyAdmin } from '../deploy/proxy.js'; import { ProxiedRouterChecker } from '../router/ProxiedRouterChecker.js'; import { verifyScale } from '../utils/decimals.js'; import { NON_ZERO_SENDER_ADDRESS, TokenType } from './config.js'; import { isCctpTokenConfig, isCollateralTokenConfig, isNativeTokenConfig, isSyntheticTokenConfig, isXERC20TokenConfig, } from './types.js'; export class HypERC20Checker extends ProxiedRouterChecker { async checkChain(chain) { let expectedChains; expectedChains = Object.keys(this.configMap); const thisChainConfig = this.configMap[chain]; if (thisChainConfig?.remoteRouters) { expectedChains = Object.keys(thisChainConfig.remoteRouters).map((remoteRouterChain) => this.multiProvider.getChainName(remoteRouterChain)); } expectedChains = expectedChains .filter((remoteRouterChain) => remoteRouterChain !== chain) .sort(); await super.checkChain(chain, expectedChains); await this.checkToken(chain); } async ownables(chain) { const contracts = this.app.getContracts(chain); const expectedConfig = this.configMap[chain]; // This is used to trigger checks for collateralProxyAdmin or collateralToken const hasCollateralProxyOverrides = expectedConfig.ownerOverrides?.collateralProxyAdmin || expectedConfig.ownerOverrides?.collateralToken; if ((isCollateralTokenConfig(this.configMap[chain]) || isXERC20TokenConfig(this.configMap[chain])) && hasCollateralProxyOverrides) { let collateralToken = await this.getCollateralToken(chain); const provider = this.multiProvider.getProvider(chain); // XERC20s are Ownable if (expectedConfig.type === TokenType.XERC20Lockbox) { const lockbox = IXERC20Lockbox__factory.connect(expectedConfig.token, provider); collateralToken = ERC20__factory.connect(await lockbox.callStatic['XERC20()'](), provider); contracts['collateralToken'] = Ownable__factory.connect(collateralToken.address, provider); } if (expectedConfig.type === TokenType.XERC20) { contracts['collateralToken'] = Ownable__factory.connect(collateralToken.address, provider); } if (await isProxy(provider, collateralToken.address)) { const admin = await proxyAdmin(provider, collateralToken.address); contracts['collateralProxyAdmin'] = ProxyAdmin__factory.connect(admin, provider); } } return filterOwnableContracts(contracts); } async checkToken(chain) { const checkERC20 = async (token, config) => { const checks = [ { method: 'symbol', violationType: 'TokenSymbolMismatch' }, { method: 'name', violationType: 'TokenNameMismatch' }, { method: 'decimals', violationType: 'TokenDecimalsMismatch' }, ]; for (const check of checks) { const actual = await token[check.method](); const expected = config[check.method]; if (expected !== undefined && actual !== expected) { const violation = { type: check.violationType, chain, expected, actual, tokenAddress: token.address, }; this.addViolation(violation); } } }; const expectedConfig = this.configMap[chain]; const hypToken = this.app.router(this.app.getContracts(chain)); // Check if configured token type matches actual token type if (isNativeTokenConfig(expectedConfig)) { try { await this.multiProvider.estimateGas(chain, { to: hypToken.address, from: NON_ZERO_SENDER_ADDRESS, value: BigNumber.from(1), }); } catch { const violation = { type: 'deployed token not payable', chain, expected: 'true', actual: 'false', tokenAddress: hypToken.address, }; this.addViolation(violation); } } else if (isSyntheticTokenConfig(expectedConfig)) { await checkERC20(hypToken, expectedConfig); } else if (isCollateralTokenConfig(expectedConfig) || isXERC20TokenConfig(expectedConfig)) { const collateralToken = await this.getCollateralToken(chain); const actualToken = await hypToken.wrappedToken(); if (!eqAddress(collateralToken.address, actualToken)) { const violation = { type: 'CollateralTokenMismatch', chain, expected: collateralToken.address, actual: actualToken, tokenAddress: hypToken.address, }; this.addViolation(violation); } } // Check all actual decimals are consistent, this should be done after checking the token type to avoid 'decimal()' calls to non collateral token that would fail const actualChainDecimals = await this.getEvmActualDecimals(); this.checkDecimalConsistency(chain, hypToken, actualChainDecimals, 'actual', true); // Check all config decimals are consistent as well const configDecimals = objMap(this.configMap, (_chain, config) => config.decimals); this.checkDecimalConsistency(chain, hypToken, configDecimals, 'config', false); } cachedAllActualDecimals = undefined; async getEvmActualDecimals() { if (this.cachedAllActualDecimals) { return this.cachedAllActualDecimals; } const entries = await Promise.all(this.getEvmChains().map(async (chain) => { const token = this.app.router(this.app.getContracts(chain)); return [chain, await this.getActualDecimals(chain, token)]; })); this.cachedAllActualDecimals = Object.fromEntries(entries); return this.cachedAllActualDecimals; } async getActualDecimals(chain, hypToken) { const expectedConfig = this.configMap[chain]; let decimals = undefined; if (isNativeTokenConfig(expectedConfig)) { decimals = this.multiProvider.getChainMetadata(chain).nativeToken?.decimals; } else if (isSyntheticTokenConfig(expectedConfig)) { decimals = await hypToken.decimals(); } else if (isCollateralTokenConfig(expectedConfig) || isXERC20TokenConfig(expectedConfig) || isCctpTokenConfig(expectedConfig)) { const collateralToken = await this.getCollateralToken(chain); decimals = await collateralToken.decimals(); } if (decimals === undefined) { throw new Error('Actual decimals not found'); } return decimals; } async getCollateralToken(chain) { const expectedConfig = this.configMap[chain]; let collateralToken = undefined; if (isCollateralTokenConfig(expectedConfig) || isCctpTokenConfig(expectedConfig) || isXERC20TokenConfig(expectedConfig)) { const provider = this.multiProvider.getProvider(chain); if (expectedConfig.type === TokenType.XERC20Lockbox) { const collateralTokenAddress = await IXERC20Lockbox__factory.connect(expectedConfig.token, provider).callStatic.ERC20(); collateralToken = ERC20__factory.connect(collateralTokenAddress, provider); } else if (expectedConfig.type === TokenType.collateralVault || expectedConfig.type === TokenType.collateralVaultRebase) { const collateralTokenAddress = await IERC4626__factory.connect(expectedConfig.token, provider).callStatic.asset(); collateralToken = ERC20__factory.connect(collateralTokenAddress, provider); } else { collateralToken = ERC20__factory.connect(expectedConfig.token, provider); } } if (!collateralToken) { throw new Error('Collateral token not found'); } return collateralToken; } checkDecimalConsistency(chain, hypToken, chainDecimals, decimalType, nonEmpty) { const definedDecimals = Object.values(chainDecimals).filter((decimals) => decimals !== undefined); const uniqueChainDecimals = new Set(definedDecimals); // Disallow partial specification: some chains define decimals while others don't const totalChains = Object.keys(chainDecimals).length; const definedCount = definedDecimals.length; if (definedCount > 0 && definedCount < totalChains) { const violation = { type: 'TokenDecimalsMismatch', chain, expected: `consistent ${decimalType} decimals specified across all chains (considering scale)`, actual: JSON.stringify(chainDecimals, (_k, v) => v === undefined ? 'undefined' : v), tokenAddress: hypToken.address, }; this.addViolation(violation); return; } // If we require non-empty and nothing is defined, report immediately if (nonEmpty && uniqueChainDecimals.size === 0) { const violation = { type: 'TokenDecimalsMismatch', chain, expected: `non-empty and consistent ${decimalType} decimals (considering scale)`, actual: JSON.stringify(chainDecimals, (_k, v) => v === undefined ? 'undefined' : v), tokenAddress: hypToken.address, }; this.addViolation(violation); return; } // If unscaled decimals agree, no need to check scale if (uniqueChainDecimals.size <= 1) return; // Build a TokenMetadata map from all chains; at this point decimals are defined on all chains const metadataMap = new Map(Object.entries(chainDecimals).map(([chn, decimals]) => [ chn, { name: this.configMap[chn]?.name ?? 'unknown', symbol: this.configMap[chn]?.symbol ?? 'unknown', decimals: decimals, scale: this.configMap[chn]?.scale ?? 1, }, ])); if (verifyScale(metadataMap)) { return; // Decimals are consistent when accounting for scale } const violation = { type: 'TokenDecimalsMismatch', chain, expected: `consistent ${decimalType} decimals (considering scale)`, actual: JSON.stringify(chainDecimals, (_k, v) => v === undefined ? 'undefined' : v), tokenAddress: hypToken.address, }; this.addViolation(violation); } } //# sourceMappingURL=checker.js.map