UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

459 lines 23.3 kB
import { constants } from 'ethers'; import { ERC20__factory, ERC721Enumerable__factory, EverclearTokenBridge__factory, IERC4626__factory, IMessageTransmitter__factory, IXERC20Lockbox__factory, MovableCollateralRouter__factory, OpL1V1NativeTokenBridge__factory, OpL2NativeTokenBridge__factory, TokenBridgeCctpBase__factory, } from '@hyperlane-xyz/core'; import { ProtocolType, addressToBytes32, assert, objFilter, objKeys, objMap, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils'; import { EvmTokenFeeModule } from '../fee/EvmTokenFeeModule.js'; import { GasRouterDeployer } from '../router/GasRouterDeployer.js'; import { resolveRouterMapConfig } from '../router/types.js'; import { TokenMetadataMap } from './TokenMetadataMap.js'; import { TokenType, gasOverhead } from './config.js'; import { getCctpFactory, hypERC20contracts, hypERC20factories, hypERC721contracts, hypERC721factories, } from './contracts.js'; import { TokenMetadataSchema, isCctpTokenConfig, isCollateralTokenConfig, isEverclearCollateralTokenConfig, isEverclearEthBridgeTokenConfig, isEverclearTokenBridgeConfig, isMovableCollateralTokenConfig, isNativeTokenConfig, isOpL1TokenConfig, isOpL2TokenConfig, isSyntheticRebaseTokenConfig, isSyntheticTokenConfig, isTokenMetadata, 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 scale = config.scale ?? 1; if (isCollateralTokenConfig(config) || isXERC20TokenConfig(config)) { return [config.token, scale, config.mailbox]; } else if (isEverclearCollateralTokenConfig(config)) { return [ config.token, scale, config.mailbox, config.everclearBridgeAddress, ]; } else if (isEverclearEthBridgeTokenConfig(config)) { return [ config.wethAddress, config.mailbox, config.everclearBridgeAddress, ]; } else if (isNativeTokenConfig(config)) { return [scale, 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, scale, config.mailbox]; } else if (isSyntheticRebaseTokenConfig(config)) { const collateralDomain = this.multiProvider.getDomainId(config.collateralChainName); return [config.decimals, scale, config.mailbox, collateralDomain]; } else if (isCctpTokenConfig(config)) { switch (config.cctpVersion) { case 'V1': return [ config.token, config.mailbox, config.messageTransmitter, config.tokenMessenger, ]; case 'V2': assert(config.maxFeeBps, 'maxFeeBps is undefined for CCTP V2 config'); assert(config.minFinalityThreshold, 'minFinalityThreshold is undefined for CCTP V2 config'); return [ config.token, config.mailbox, config.messageTransmitter, config.tokenMessenger, config.maxFeeBps, 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 (isCollateralTokenConfig(config) || isXERC20TokenConfig(config) || isNativeTokenConfig(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) { const metadataMap = new TokenMetadataMap(); const priorityGetter = (type) => { return ['collateral', 'native'].indexOf(type); }; const sortedEntries = Object.entries(configMap).sort(([, a], [, b]) => priorityGetter(b.type) - priorityGetter(a.type)); for (const [chain, config] of sortedEntries) { if (isTokenMetadata(config)) { metadataMap.set(chain, TokenMetadataSchema.parse(config)); } if (multiProvider.getProtocol(chain) !== ProtocolType.Ethereum) { // If the config didn't specify the token metadata, we can only now // derive it for Ethereum chains. So here we skip non-Ethereum chains. continue; } if (isNativeTokenConfig(config) || isEverclearEthBridgeTokenConfig(config)) { const nativeToken = multiProvider.getChainMetadata(chain).nativeToken; if (nativeToken) { metadataMap.update(chain, TokenMetadataSchema.parse({ ...nativeToken, })); continue; } } if (isCollateralTokenConfig(config) || isXERC20TokenConfig(config) || isCctpTokenConfig(config) || isEverclearCollateralTokenConfig(config)) { const provider = multiProvider.getProvider(chain); if (config.isNft) { const erc721 = ERC721Enumerable__factory.connect(config.token, provider); const [name, symbol] = await Promise.all([ erc721.name(), erc721.symbol(), ]); metadataMap.update(chain, TokenMetadataSchema.parse({ name, symbol, })); continue; } let token; switch (config.type) { case TokenType.XERC20Lockbox: token = await IXERC20Lockbox__factory.connect(config.token, provider).callStatic.ERC20(); break; case TokenType.collateralVault: token = await IERC4626__factory.connect(config.token, provider).callStatic.asset(); break; default: token = config.token; break; } const erc20 = ERC20__factory.connect(token, provider); const [name, symbol, decimals] = await Promise.all([ erc20.name(), erc20.symbol(), erc20.decimals(), ]); metadataMap.update(chain, TokenMetadataSchema.parse({ name, symbol, decimals, })); } } metadataMap.finalize(); return metadataMap; } 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, }); await this.multiProvider.handleTx(chain, tokenBridge.addDomains(remoteDomains)); })); } 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 ?? []); for (const rebalancer of rebalancers) { await this.multiProvider.handleTx(chain, movableToken.addRebalancer(rebalancer)); } })); } 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)); for (const bridgeConfig of bridgesToAllowOnRouter) { await this.multiProvider.handleTx(chain, movableToken.addBridge(bridgeConfig.domain, bridgeConfig.bridge)); } })); } 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)); for (const bridgeConfig of filteredTokenApprovalTxs) { await this.multiProvider.handleTx(chain, movableToken.approveTokenForBridge(bridgeConfig.token, bridgeConfig.bridge)); } })); } 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); for (const [domainId, feeConfig] of Object.entries(resolvedFeeParamsConfig)) { await this.multiProvider.handleTx(chain, everclearTokenBridge.setFeeParams(domainId, feeConfig.fee, feeConfig.deadline, feeConfig.signature)); } })); } 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), })); await this.multiProvider.handleTx(chain, everclearTokenBridge.setOutputAssetsBatch(assets)); })); } async deploy(configMap) { 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 deployedContractsMap = await super.deploy(resolvedConfigMap); // Configure CCTP domains after all routers are deployed and remotes are enrolled (in super.deploy) await this.configureCctpDomains(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 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 (this.multiProvider.getProtocol(chain) !== ProtocolType.Ethereum) { this.logger.debug(`Skipping token fee on non-EVM chain ${chain}`); return; } this.logger.debug(`Deploying token fee on ${chain}...`); const processedTokenFee = await EvmTokenFeeModule.expandConfig({ config: tokenFeeInput, multiProvider: this.multiProvider, chainName: chain, }); const module = await EvmTokenFeeModule.create({ multiProvider: this.multiProvider, chain, config: processedTokenFee, }); const router = this.router(deployedContractsMap[chain]); const { deployedFee } = module.serialize(); const tx = await router.setFeeRecipient(deployedFee); 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