UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

658 lines 37 kB
import { compareVersions } from 'compare-versions'; import { BigNumber, constants } from 'ethers'; import { CrossCollateralRouter__factory, ERC20__factory, MailboxClient__factory, EverclearTokenBridge__factory, IMessageTransmitter__factory, MovableCollateralRouter__factory, OpL1V1NativeTokenBridge__factory, OpL2NativeTokenBridge__factory, PackageVersioned__factory, TokenBridgeOft__factory, TokenBridgeCctpBase__factory, TokenBridgeCctpV2__factory, TokenBridgeDepositAddress__factory, TokenRouter__factory, } from '@hyperlane-xyz/core'; import { addressToBytes32, isEVMLike, assert, objFilter, objKeys, objMap, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils'; import { EvmTokenFeeModule } from '../fee/EvmTokenFeeModule.js'; import { PredicateWrapperDeployer } from '../predicate/PredicateDeployer.js'; import { GasRouterDeployer } from '../router/GasRouterDeployer.js'; import { resolveRouterMapConfig } from '../router/types.js'; import { normalizeScale } from '../utils/decimals.js'; import { setRateLimitedIsmRecipient } from '../utils/ism.js'; import { CCTP_PPM_PRECISION_VERSION, CCTP_PPM_STORAGE_VERSION, } from './EvmWarpRouteReader.js'; import { gasOverhead } from './config.js'; import { resolveTokenFeeAddress } from './configUtils.js'; import { getCctpFactory, hypERC20contracts, hypERC20factories, hypERC721contracts, hypERC721factories, } from './contracts.js'; import { deriveTokenMetadata } from './tokenMetadataUtils.js'; import { isCctpTokenConfig, isCollateralTokenConfig, isEverclearCollateralTokenConfig, isEverclearEthBridgeTokenConfig, isEverclearTokenBridgeConfig, isDepositAddressTokenConfig, isMovableCollateralTokenConfig, isCrossCollateralTokenConfig, isNativeTokenConfig, isOftTokenConfig, isOpL1TokenConfig, isOpL2TokenConfig, isSyntheticRebaseTokenConfig, isSyntheticTokenConfig, isXERC20TokenConfig, } from './types.js'; // initialize(address _hook, address _owner) const OP_L2_INITIALIZE_SIGNATURE = 'initialize(address,address)'; // initialize(address _owner, string[] memory _urls) const OP_L1_INITIALIZE_SIGNATURE = 'initialize(address,string[])'; // initialize(address _hook, address _owner, string[] memory __urls) const CCTP_INITIALIZE_SIGNATURE = 'initialize(address,address,string[])'; // initialize(address _hook, address _owner) const EVERCLEAR_TOKEN_BRIDGE_INITIALIZE_SIGNATURE = 'initialize(address,address)'; export const TOKEN_INITIALIZE_SIGNATURE = (contractName) => { switch (contractName) { case 'OPL2TokenBridgeNative': assert(OpL2NativeTokenBridge__factory.createInterface().functions[OP_L2_INITIALIZE_SIGNATURE], 'missing expected initialize function'); return OP_L2_INITIALIZE_SIGNATURE; case 'OpL1TokenBridgeNative': assert(OpL1V1NativeTokenBridge__factory.createInterface().functions[OP_L1_INITIALIZE_SIGNATURE], 'missing expected initialize function'); return OP_L1_INITIALIZE_SIGNATURE; case 'TokenBridgeCctp': assert(TokenBridgeCctpBase__factory.createInterface().functions[CCTP_INITIALIZE_SIGNATURE], 'missing expected initialize function'); return CCTP_INITIALIZE_SIGNATURE; case 'EverclearTokenBridge': case 'EverclearEthBridge': assert(EverclearTokenBridge__factory.createInterface().functions[EVERCLEAR_TOKEN_BRIDGE_INITIALIZE_SIGNATURE], 'missing expected initialize function'); return EVERCLEAR_TOKEN_BRIDGE_INITIALIZE_SIGNATURE; default: return 'initialize'; } }; class TokenDeployer extends GasRouterDeployer { constructor(multiProvider, factories, loggerName, ismFactory, contractVerifier, concurrentDeploy = true) { super(multiProvider, factories, { logger: rootLogger.child({ module: loggerName }), ismFactory, contractVerifier, concurrentDeploy, }); // factories not used in deploy } async constructorArgs(_, config) { // TODO: derive as specified in https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/5296 const { numerator, denominator } = normalizeScale(config.scale); if (isCollateralTokenConfig(config) || isXERC20TokenConfig(config) || isCrossCollateralTokenConfig(config)) { return [config.token, numerator, denominator, config.mailbox]; } else if (isEverclearCollateralTokenConfig(config)) { return [ config.token, numerator, denominator, config.mailbox, config.everclearBridgeAddress, ]; } else if (isEverclearEthBridgeTokenConfig(config)) { return [ config.wethAddress, config.mailbox, config.everclearBridgeAddress, ]; } else if (isNativeTokenConfig(config)) { return [numerator, denominator, config.mailbox]; } else if (isOpL2TokenConfig(config)) { return [config.mailbox, config.l2Bridge]; } else if (isOpL1TokenConfig(config)) { return [config.mailbox, config.portal]; } else if (isSyntheticTokenConfig(config)) { assert(config.decimals, 'decimals is undefined for config'); // decimals must be defined by this point return [config.decimals, numerator, denominator, config.mailbox]; } else if (isSyntheticRebaseTokenConfig(config)) { const collateralDomain = this.multiProvider.getDomainId(config.collateralChainName); return [ config.decimals, numerator, denominator, config.mailbox, collateralDomain, ]; } else if (isOftTokenConfig(config)) { return [config.oft, config.owner]; } else if (isDepositAddressTokenConfig(config)) { return [config.token, config.owner]; } else if (isCctpTokenConfig(config)) { switch (config.cctpVersion) { case 'V1': return [ config.token, config.mailbox, config.messageTransmitter, config.tokenMessenger, ]; case 'V2': { assert(config.maxFeeBps !== undefined, 'maxFeeBps is undefined for CCTP V2 config'); assert(config.minFinalityThreshold !== undefined, 'minFinalityThreshold is undefined for CCTP V2 config'); // Convert bps to ppm (parts per million) for contract precision // 1 bps = 100 ppm, supports fractional bps (e.g., 1.3 bps = 130 ppm) const maxFeePpm = Math.round(config.maxFeeBps * 100); return [ config.token, config.mailbox, config.messageTransmitter, config.tokenMessenger, maxFeePpm, config.minFinalityThreshold, ]; } default: throw new Error('Unsupported CCTP version'); } } else { throw new Error('Unknown token type when constructing arguments'); } } initializeFnSignature(name) { return TOKEN_INITIALIZE_SIGNATURE(name); } async initializeArgs(chain, config) { const signer = await this.multiProvider.getSigner(chain).getAddress(); const defaultArgs = [ config.hook ?? constants.AddressZero, config.interchainSecurityModule ?? constants.AddressZero, // TransferOwnership will happen later in RouterDeployer signer, ]; if (isOftTokenConfig(config)) { // OFT is deployed unproxied — owner is set in constructor, no initialize throw new Error('OFT does not use initialize'); } else if (isDepositAddressTokenConfig(config)) { throw new Error('Direct bridge adapters do not use initialize'); } else if (isCollateralTokenConfig(config) || isXERC20TokenConfig(config) || isNativeTokenConfig(config) || isCrossCollateralTokenConfig(config)) { return defaultArgs; } else if (isEverclearCollateralTokenConfig(config) || isEverclearEthBridgeTokenConfig(config)) { return [config.hook ?? constants.AddressZero, config.owner]; } else if (isOpL2TokenConfig(config)) { return [config.hook ?? constants.AddressZero, config.owner]; } else if (isOpL1TokenConfig(config)) { return [config.owner, config.urls]; } else if (isCctpTokenConfig(config)) { return [config.hook ?? constants.AddressZero, config.owner, config.urls]; } else if (isSyntheticTokenConfig(config)) { return [ config.initialSupply ?? 0, config.name, config.symbol, ...defaultArgs, ]; } else if (isSyntheticRebaseTokenConfig(config)) { return [0, config.name, config.symbol, ...defaultArgs]; } else { throw new Error('Unknown collateral type when initializing arguments'); } } static async deriveTokenMetadata(multiProvider, configMap) { return deriveTokenMetadata(multiProvider, configMap); } async configureCctpDomains(configMap, deployedContractsMap) { const cctpConfigs = objFilter(configMap, (_, config) => isCctpTokenConfig(config)); const circleDomains = await promiseObjAll(objMap(cctpConfigs, (chain, config) => IMessageTransmitter__factory.connect(config.messageTransmitter, this.multiProvider.getProvider(chain)).localDomain())); const domains = Object.entries(circleDomains).map(([chain, circle]) => ({ hyperlane: this.multiProvider.getDomainId(chain), circle, })); if (domains.length === 0) { return; } await promiseObjAll(objMap(cctpConfigs, async (chain, _config) => { const router = this.router(deployedContractsMap[chain]).address; const tokenBridge = TokenBridgeCctpBase__factory.connect(router, this.multiProvider.getSigner(chain)); const remoteDomains = domains.filter((domain) => domain.hyperlane !== this.multiProvider.getDomainId(chain)); this.logger.info(`Mapping Circle domains on ${chain}`, { remoteDomains, }); const overrides = this.multiProvider.getTransactionOverrides(chain); await this.multiProvider.handleTx(chain, tokenBridge.addDomains(remoteDomains, overrides)); })); } async configureCctpV2MaxFee(configMap, deployedContractsMap) { const cctpV2Configs = objFilter(configMap, (_, config) => isCctpTokenConfig(config) && config.cctpVersion === 'V2' && config.maxFeeBps !== undefined); await promiseObjAll(objMap(cctpV2Configs, async (chain, config) => { const router = this.router(deployedContractsMap[chain]).address; const tokenBridgeV2 = TokenBridgeCctpV2__factory.connect(router, this.multiProvider.getSigner(chain)); // Check contract version to determine ppm conversion and function name const versionedContract = PackageVersioned__factory.connect(router, this.multiProvider.getProvider(chain)); const contractVersion = await versionedContract.PACKAGE_VERSION(); const usesPpmStorage = compareVersions(contractVersion, CCTP_PPM_STORAGE_VERSION) >= 0; const usesPpmName = compareVersions(contractVersion, CCTP_PPM_PRECISION_VERSION) >= 0; // Convert bps to ppm for contracts that store fees in ppm (>= 10.2.0) const targetFee = usesPpmStorage ? Math.round(config.maxFeeBps * 100) : config.maxFeeBps; // Read current fee: >= 11.0.0 uses maxFeePpm(), older uses maxFeeBps() const currentMaxFee = usesPpmName ? await tokenBridgeV2.maxFeePpm() : BigNumber.from(await tokenBridgeV2.provider.call({ to: router, // maxFeeBps() selector data: '0xbf769a3f', })); if (currentMaxFee.toNumber() !== targetFee) { const currentFeeBps = usesPpmStorage ? currentMaxFee.toNumber() / 100 : currentMaxFee.toNumber(); this.logger.info(`Setting maxFeePpm on ${chain} from ${currentFeeBps} bps to ${config.maxFeeBps} bps${usesPpmStorage ? ' (stored as ppm)' : ''}`); // >= 11.0.0 uses setMaxFeePpm(), older uses setMaxFeeBps() const overrides = this.multiProvider.getTransactionOverrides(chain); if (usesPpmName) { await this.multiProvider.handleTx(chain, tokenBridgeV2.setMaxFeePpm(targetFee, overrides)); } else { await this.multiProvider.handleTx(chain, tokenBridgeV2.signer.sendTransaction({ to: router, // setMaxFeeBps(uint256) selector + abi-encoded targetFee data: '0x246d4569' + BigNumber.from(targetFee) .toHexString() .slice(2) .padStart(64, '0'), ...overrides, })); } } })); } async configureDepositAddressDestinations(configMap, deployedContractsMap) { const depositConfigs = objFilter(configMap, (_, config) => isDepositAddressTokenConfig(config)); await promiseObjAll(objMap(depositConfigs, async (chain, config) => { const router = this.router(deployedContractsMap[chain]).address; const tokenBridge = TokenBridgeDepositAddress__factory.connect(router, this.multiProvider.getSigner(chain)); const resolvedConfigs = resolveRouterMapConfig(this.multiProvider, config.destinationConfigs); for (const [domainId, destinationConfig] of Object.entries(resolvedConfigs)) { for (const [recipient, recipientConfig] of Object.entries(destinationConfig)) { this.logger.info(`Setting deposit-address bridge destination config on ${chain}`, { destination: domainId, depositAddress: recipientConfig.depositAddress, recipient, }); await this.multiProvider.handleTx(chain, tokenBridge.addDestinationConfig(Number(domainId), recipientConfig.depositAddress, recipient, BigNumber.from(recipientConfig.feeBps ?? 0))); } } })); } async configureOftDomains(configMap, deployedContractsMap) { const oftConfigs = objFilter(configMap, (_, config) => isOftTokenConfig(config)); await promiseObjAll(objMap(oftConfigs, async (chain, config) => { const router = this.router(deployedContractsMap[chain]).address; const tokenBridge = TokenBridgeOft__factory.connect(router, this.multiProvider.getSigner(chain)); const resolvedMappings = resolveRouterMapConfig(this.multiProvider, config.domainMappings); for (const [domainId, lzEid] of Object.entries(resolvedMappings)) { this.logger.info(`Adding OFT domain mapping on ${chain}`, { hyperlaneDomain: domainId, lzEid, }); const overrides = this.multiProvider.getTransactionOverrides(chain); await this.multiProvider.handleTx(chain, tokenBridge.addDomain(Number(domainId), lzEid, overrides)); } if (config.extraOptions) { this.logger.info(`Setting OFT extra options on ${chain}`); const overrides = this.multiProvider.getTransactionOverrides(chain); await this.multiProvider.handleTx(chain, tokenBridge.setExtraOptions(config.extraOptions, overrides)); } })); } async setRebalancers(configMap, deployedContractsMap) { await promiseObjAll(objMap(configMap, async (chain, config) => { if (!isMovableCollateralTokenConfig(config)) { return; } const router = this.router(deployedContractsMap[chain]).address; const movableToken = MovableCollateralRouter__factory.connect(router, this.multiProvider.getSigner(chain)); const rebalancers = Array.from(config.allowedRebalancers ?? []); const overrides = this.multiProvider.getTransactionOverrides(chain); for (const rebalancer of rebalancers) { await this.multiProvider.handleTx(chain, movableToken.addRebalancer(rebalancer, overrides)); } })); } async setAllowedBridges(configMap, deployedContractsMap) { await promiseObjAll(objMap(configMap, async (chain, config) => { if (!isMovableCollateralTokenConfig(config)) { return; } const router = this.router(deployedContractsMap[chain]); const movableToken = MovableCollateralRouter__factory.connect(router.address, this.multiProvider.getSigner(chain)); const bridgesToAllow = Object.entries(resolveRouterMapConfig(this.multiProvider, config.allowedRebalancingBridges ?? {})).flatMap(([domain, allowedBridgesToAdd]) => { return allowedBridgesToAdd.map((bridgeToAdd) => { return { domain: Number(domain), bridge: bridgeToAdd.bridge, }; }); }); // Filter out domains that are not enrolled to avoid errors const routerDomains = await router.domains(); const bridgesToAllowOnRouter = bridgesToAllow.filter(({ domain }) => routerDomains.includes(domain)); const overrides = this.multiProvider.getTransactionOverrides(chain); for (const bridgeConfig of bridgesToAllowOnRouter) { await this.multiProvider.handleTx(chain, movableToken.addBridge(bridgeConfig.domain, bridgeConfig.bridge, overrides)); } })); } async setBridgesTokenApprovals(configMap, deployedContractsMap) { await promiseObjAll(objMap(configMap, async (chain, config) => { if (!isMovableCollateralTokenConfig(config)) { return; } const router = this.router(deployedContractsMap[chain]).address; const movableToken = MovableCollateralRouter__factory.connect(router, this.multiProvider.getSigner(chain)); const tokenApprovalTxs = Object.values(config.allowedRebalancingBridges ?? {}).flatMap((allowedBridgesToAdd) => { return allowedBridgesToAdd.flatMap((bridgeToAdd) => { return (bridgeToAdd.approvedTokens ?? []).map((token) => { return { bridge: bridgeToAdd.bridge, token, }; }); }); }); // Find which bridges already have the required approval to avoid // safeApproval to fail because it requires approvals to be set to 0 // before setting a new value const tokens = new Set(tokenApprovalTxs.map(({ token }) => token)); const bridgesWithAllowanceAlreadySet = Object.fromEntries(Array.from(tokens).map((token) => [token, new Set()])); await Promise.all(tokenApprovalTxs.map(async ({ bridge, token }) => { const tokenInstance = ERC20__factory.connect(token, this.multiProvider.getSigner(chain)); const currentAllowance = await tokenInstance.allowance(movableToken.address, bridge); if (currentAllowance.gt(0)) { bridgesWithAllowanceAlreadySet[token].add(bridge); } })); const filteredTokenApprovalTxs = tokenApprovalTxs.filter(({ bridge, token }) => bridgesWithAllowanceAlreadySet[token] && !bridgesWithAllowanceAlreadySet[token].has(bridge)); const overrides = this.multiProvider.getTransactionOverrides(chain); for (const bridgeConfig of filteredTokenApprovalTxs) { await this.multiProvider.handleTx(chain, movableToken.approveTokenForBridge(bridgeConfig.token, bridgeConfig.bridge, overrides)); } })); } async setEverclearFeeParams(configMap, deployedContractsMap) { await promiseObjAll(objMap(configMap, async (chain, config) => { if (!isEverclearTokenBridgeConfig(config)) { return; } const router = this.router(deployedContractsMap[chain]).address; const everclearTokenBridge = EverclearTokenBridge__factory.connect(router, this.multiProvider.getSigner(chain)); const resolvedFeeParamsConfig = resolveRouterMapConfig(this.multiProvider, config.everclearFeeParams); const overrides = this.multiProvider.getTransactionOverrides(chain); for (const [domainId, feeConfig] of Object.entries(resolvedFeeParamsConfig)) { await this.multiProvider.handleTx(chain, everclearTokenBridge.setFeeParams(domainId, feeConfig.fee, feeConfig.deadline, feeConfig.signature, overrides)); } })); } async setEverclearOutputAssets(configMap, deployedContractsMap) { await promiseObjAll(objMap(configMap, async (chain, config) => { if (!isEverclearTokenBridgeConfig(config)) { return; } const router = this.router(deployedContractsMap[chain]).address; const everclearTokenBridge = EverclearTokenBridge__factory.connect(router, this.multiProvider.getSigner(chain)); const remoteOutputAddresses = resolveRouterMapConfig(this.multiProvider, config.outputAssets); const assets = Object.entries(remoteOutputAddresses).map(([domainId, outputAsset]) => ({ destination: parseInt(domainId), outputAsset: addressToBytes32(outputAsset), })); const overrides = this.multiProvider.getTransactionOverrides(chain); await this.multiProvider.handleTx(chain, everclearTokenBridge.setOutputAssetsBatch(assets, overrides)); })); } async deployPredicateWrappers(configMap, deployedContractsMap) { await promiseObjAll(objMap(configMap, async (chain, config) => { if (!config.predicateWrapper) { return; } const router = this.router(deployedContractsMap[chain]); const factoryContracts = this.options.ismFactory?.getContracts(chain); assert(factoryContracts?.staticAggregationHookFactory, `staticAggregationHookFactory not found for ${chain}. Ensure proxy factories are deployed.`); const predicateDeployer = new PredicateWrapperDeployer(this.multiProvider, factoryContracts.staticAggregationHookFactory, this.logger); // Token address is fetched from router.token() in PredicateRouterWrapper constructor. // config.predicateWrapper.owner (from the original configMap) is used for wrapper // ownership — it's explicit in the schema rather than read from on-chain, so it // correctly points to the intended final owner even before transferOwnership runs. const result = await predicateDeployer.deployAndConfigure(chain, router.address, config.predicateWrapper, config.type); const signerRouter = TokenRouter__factory.connect(router.address, this.multiProvider.getSigner(chain)); const txOverrides = this.multiProvider.getTransactionOverrides(chain); await this.multiProvider.handleTx(chain, signerRouter.setHook(result.aggregationHookAddress, txOverrides)); })); } async enrollCrossCollateralRouters(configMap, deployedContractsMap) { await promiseObjAll(objMap(configMap, async (chain, config) => { if (!isCrossCollateralTokenConfig(config)) { return; } if (!config.crossCollateralRouters || Object.keys(config.crossCollateralRouters).length === 0) { return; } const router = this.router(deployedContractsMap[chain]).address; const crossCollateralRouter = CrossCollateralRouter__factory.connect(router, this.multiProvider.getSigner(chain)); const resolvedRouters = resolveRouterMapConfig(this.multiProvider, config.crossCollateralRouters); const domains = []; const routers = []; for (const [domainId, routerAddresses] of Object.entries(resolvedRouters)) { for (const routerAddr of routerAddresses) { domains.push(Number(domainId)); routers.push(addressToBytes32(routerAddr)); } } if (domains.length > 0) { this.logger.info(`Batch enrolling ${domains.length} routers for ${chain}`); const overrides = this.multiProvider.getTransactionOverrides(chain); await this.multiProvider.handleTx(chain, crossCollateralRouter.enrollCrossCollateralRouters(domains, routers, overrides)); } })); } // Wire rate-limited ISMs BEFORE ownership transfer so that // setInterchainSecurityModule succeeds regardless of config.owner. // Handles both top-level RateLimitedIsm and ISMs nested inside composites // (aggregation, routing, etc.) by setting `recipient` on every RATE_LIMITED // node in the tree before deploying. async setRateLimitedIsms(rateLimitedIsms, configMap, deployedContractsMap) { const ismFactory = this.options.ismFactory; assert(ismFactory, 'ismFactory is required to deploy RateLimitedIsm — pass it to the deployer constructor'); await promiseObjAll(objMap(rateLimitedIsms, async (chain, ismConfig) => { const router = this.router(deployedContractsMap[chain]); const mailbox = configMap[chain].mailbox; const defaultOwner = configMap[chain].owner; const resolvedIsm = setRateLimitedIsmRecipient(ismConfig, router.address, defaultOwner); const deployedIsm = await ismFactory.deploy({ destination: chain, config: resolvedIsm, mailbox, }); const tokenContract = MailboxClient__factory.connect(router.address, this.multiProvider.getProvider(chain)); await this.multiProvider.sendTransaction(chain, { to: router.address, data: tokenContract.interface.encodeFunctionData('setInterchainSecurityModule', [deployedIsm.address]), }); })); } async deploy(configMap, rateLimitedIsms) { // Fail fast if any chain requires a predicate wrapper but lacks the factory. // Checked before any on-chain work to avoid partial deployments. for (const [chain, config] of Object.entries(configMap)) { if (!('predicateWrapper' in config) || !config.predicateWrapper) continue; const factoryContracts = this.options.ismFactory?.getContracts(chain); assert(factoryContracts?.staticAggregationHookFactory, `staticAggregationHookFactory not found for ${chain}. Ensure proxy factories are deployed.`); } // Fail fast if rateLimitedIsms are requested but ismFactory is missing. // setRateLimitedIsms runs after super.deploy(), so catching this early // prevents partial on-chain work before hitting the same assert there. if (rateLimitedIsms && Object.keys(rateLimitedIsms).length > 0) { assert(this.options.ismFactory, 'ismFactory is required to deploy RateLimitedIsm — pass it to the deployer constructor'); } let tokenMetadataMap; try { tokenMetadataMap = await TokenDeployer.deriveTokenMetadata(this.multiProvider, configMap); } catch (err) { this.logger.error('Failed to derive token metadata', err, configMap); throw err; } const resolvedConfigMap = await promiseObjAll(objMap(configMap, async (chain, config) => ({ name: tokenMetadataMap.getName(chain), decimals: tokenMetadataMap.getDecimals(chain), symbol: tokenMetadataMap.getSymbol(chain) || tokenMetadataMap.getDefaultSymbol(), scale: tokenMetadataMap.getScale(chain), gas: gasOverhead(config.type), ...config, // override intermediate owner to the signer owner: await this.multiProvider.getSigner(chain).getAddress(), }))); const directBridgeContracts = {}; const oftContracts = {}; for (const [chain, config] of Object.entries(resolvedConfigMap)) { if (isDepositAddressTokenConfig(config)) { const contractKey = this.routerContractKey(config); const constructorArgs = await this.constructorArgs(chain, config); const contract = await this.deployContractWithName(chain, contractKey, this.routerContractName(config), constructorArgs); directBridgeContracts[chain] = { [contractKey]: contract }; delete resolvedConfigMap[chain]; continue; } if (isOftTokenConfig(config)) { const contractKey = this.routerContractKey(config); const constructorArgs = await this.constructorArgs(chain, config); const contract = await this.deployContract(chain, contractKey, constructorArgs); oftContracts[chain] = { [contractKey]: contract }; delete resolvedConfigMap[chain]; } } const deployedContractsMap = Object.keys(resolvedConfigMap).length > 0 ? await super.deploy(resolvedConfigMap) : // CAST: with no router-style deploys, the accumulated in-memory deploy state already // matches the public return shape even though the base field is declared less precisely. this.deployedContracts; for (const [chain, contracts] of Object.entries(directBridgeContracts)) { this.addDeployedContracts(chain, contracts); deployedContractsMap[chain] = { ...deployedContractsMap[chain], ...contracts, }; } // Now safe to merge direct-bridge / OFT entries — Router-specific methods have already run for (const [chain, contracts] of Object.entries(oftContracts)) { this.addDeployedContracts(chain, contracts); deployedContractsMap[chain] = { ...deployedContractsMap[chain], ...contracts, }; } // Configure CCTP domains after all routers are deployed and remotes are enrolled (in super.deploy) await this.configureCctpDomains(configMap, deployedContractsMap); // Set maxFeeBps for CCTP V2 routers (constructor sets it for direct deploys, this handles proxies) await this.configureCctpV2MaxFee(configMap, deployedContractsMap); await this.configureDepositAddressDestinations(configMap, deployedContractsMap); // Configure OFT domain mappings (Hyperlane domain → LZ EID) await this.configureOftDomains(configMap, deployedContractsMap); await this.setRebalancers(configMap, deployedContractsMap); await this.setAllowedBridges(configMap, deployedContractsMap); await this.setBridgesTokenApprovals(configMap, deployedContractsMap); await this.setEverclearFeeParams(configMap, deployedContractsMap); await this.setEverclearOutputAssets(configMap, deployedContractsMap); await this.deployPredicateWrappers(configMap, deployedContractsMap); await this.enrollCrossCollateralRouters(configMap, deployedContractsMap); // RateLimitedIsms are wired after enrollment. A brief window exists where // the token's effective ISM is the mailbox defaultIsm, but it is inert on a // fresh deploy: no remote peers are enrolled yet, so no valid inbound message // can arrive and be handled by the token during that window. if (rateLimitedIsms && Object.keys(rateLimitedIsms).length > 0) { await this.setRateLimitedIsms(rateLimitedIsms, configMap, deployedContractsMap); } await super.transferOwnership(deployedContractsMap, configMap); return deployedContractsMap; } } export class HypERC20Deployer extends TokenDeployer { constructor(multiProvider, ismFactory, contractVerifier, concurrentDeploy = true) { super(multiProvider, hypERC20factories, 'HypERC20Deployer', ismFactory, contractVerifier, concurrentDeploy); } router(contracts) { for (const key of objKeys(hypERC20factories)) { if (contracts[key]) { return contracts[key]; } } throw new Error('No matching contract found'); } routerContractKey(config) { assert(config.type in hypERC20factories, 'Invalid ERC20 token type'); return config.type; } routerContractName(config) { // Handle CCTP version-specific contract names if (isCctpTokenConfig(config)) { return `TokenBridgeCctp${config.cctpVersion}`; } return hypERC20contracts[this.routerContractKey(config)]; } // Override deployContractFromFactory to handle CCTP version selection async deployContractFromFactory(chain, factory, contractName, constructorArgs, initializeArgs, shouldRecover = true, implementationAddress) { // For CCTP contracts, use the version-specific factory if (contractName.startsWith('TokenBridgeCctp')) { factory = getCctpFactory(contractName.split('TokenBridgeCctp')[1]); } // Use the default deployment for other types return super.deployContractFromFactory(chain, factory, contractName, constructorArgs, initializeArgs, shouldRecover, implementationAddress); } async deployAndConfigureTokenFees(deployedContractsMap, configMap) { await Promise.all(Object.keys(deployedContractsMap).map(async (chain) => { const config = configMap[chain]; const tokenFeeInput = config?.tokenFee; if (!tokenFeeInput) return; if (!isEVMLike(this.multiProvider.getProtocol(chain))) { this.logger.debug(`Skipping token fee on non-EVM chain ${chain}`); return; } const router = this.router(deployedContractsMap[chain]); const resolvedFeeInput = resolveTokenFeeAddress(tokenFeeInput, router.address, config); this.logger.debug(`Deploying token fee on ${chain}...`); const processedTokenFee = await EvmTokenFeeModule.expandConfig({ config: resolvedFeeInput, multiProvider: this.multiProvider, chainName: chain, }); const module = await EvmTokenFeeModule.create({ multiProvider: this.multiProvider, chain, config: processedTokenFee, }); const { deployedFee } = module.serialize(); const overrides = this.multiProvider.getTransactionOverrides(chain); const tx = await router.setFeeRecipient(deployedFee, overrides); await this.multiProvider.handleTx(chain, tx); })); } } export class HypERC721Deployer extends TokenDeployer { constructor(multiProvider, ismFactory, contractVerifier) { super(multiProvider, hypERC721factories, 'HypERC721Deployer', ismFactory, contractVerifier); } router(contracts) { for (const key of objKeys(hypERC721factories)) { if (contracts[key]) { return contracts[key]; } } throw new Error('No matching contract found'); } routerContractKey(config) { assert(config.type in hypERC721factories, 'Invalid ERC721 token type'); return config.type; } routerContractName(config) { return hypERC721contracts[this.routerContractKey(config)]; } } //# sourceMappingURL=deploy.js.map