@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
658 lines • 37 kB
JavaScript
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