diff --git a/.changeset/eleven-cows-dance.md b/.changeset/eleven-cows-dance.md
new file mode 100644
index 0000000000..adc16422bd
--- /dev/null
+++ b/.changeset/eleven-cows-dance.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/sdk': minor
+---
+
+Added ZKSync specific deployment logic and artifact related utils
diff --git a/typescript/sdk/src/deploy/proxy.ts b/typescript/sdk/src/deploy/proxy.ts
index 8749e433a9..9e28e4f65a 100644
--- a/typescript/sdk/src/deploy/proxy.ts
+++ b/typescript/sdk/src/deploy/proxy.ts
@@ -1,4 +1,5 @@
import { ethers } from 'ethers';
+import { Provider as ZKSyncProvider } from 'zksync-ethers';
import { ProxyAdmin__factory } from '@hyperlane-xyz/core';
import { Address, ChainId, eqAddress } from '@hyperlane-xyz/utils';
@@ -7,6 +8,8 @@ import { transferOwnershipTransactions } from '../contracts/contracts.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { DeployedOwnableConfig } from '../types.js';
+type NetworkProvider = ethers.providers.Provider | ZKSyncProvider;
+
export type UpgradeConfig = {
timelock: {
delay: number;
@@ -19,7 +22,7 @@ export type UpgradeConfig = {
};
export async function proxyImplementation(
- provider: ethers.providers.Provider,
+ provider: NetworkProvider,
proxy: Address,
): Promise
{
// Hardcoded storage slot for implementation per EIP-1967
@@ -31,7 +34,7 @@ export async function proxyImplementation(
}
export async function isInitialized(
- provider: ethers.providers.Provider,
+ provider: NetworkProvider,
contract: Address,
): Promise {
// Using OZ's Initializable 4.9 which keeps it at the 0x0 slot
@@ -43,7 +46,7 @@ export async function isInitialized(
}
export async function proxyAdmin(
- provider: ethers.providers.Provider,
+ provider: NetworkProvider,
proxy: Address,
): Promise {
// Hardcoded storage slot for admin per EIP-1967
@@ -66,7 +69,7 @@ export function proxyConstructorArgs(
}
export async function isProxy(
- provider: ethers.providers.Provider,
+ provider: NetworkProvider,
proxy: Address,
): Promise {
const admin = await proxyAdmin(provider, proxy);
diff --git a/typescript/sdk/src/deploy/proxyFactoryUtils.ts b/typescript/sdk/src/deploy/proxyFactoryUtils.ts
new file mode 100644
index 0000000000..ecdbd51dd1
--- /dev/null
+++ b/typescript/sdk/src/deploy/proxyFactoryUtils.ts
@@ -0,0 +1,16 @@
+import { ethers } from 'ethers';
+
+import { proxyFactoryFactories } from './contracts.js';
+import { ProxyFactoryFactoriesAddresses } from './types.js';
+
+/**
+ * Creates a default ProxyFactoryFactoriesAddresses object with all values set to ethers.constants.AddressZero.
+ * @returns {ProxyFactoryFactoriesAddresses} An object with all factory addresses set to AddressZero.
+ */
+export function createDefaultProxyFactoryFactories(): ProxyFactoryFactoriesAddresses {
+ const defaultAddress = ethers.constants.AddressZero;
+ return Object.keys(proxyFactoryFactories).reduce((acc, key) => {
+ acc[key as keyof ProxyFactoryFactoriesAddresses] = defaultAddress; // Type assertion added here
+ return acc;
+ }, {} as ProxyFactoryFactoriesAddresses);
+}
diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts
index 76e749fd95..8f1ea0c7c4 100644
--- a/typescript/sdk/src/hook/types.ts
+++ b/typescript/sdk/src/hook/types.ts
@@ -38,6 +38,21 @@ export enum HookType {
ARB_L2_TO_L1 = 'arbL2ToL1Hook',
}
+export const HookTypeToContractNameMap: Record<
+ Exclude,
+ string
+> = {
+ [HookType.MERKLE_TREE]: 'merkleTreeHook',
+ [HookType.INTERCHAIN_GAS_PAYMASTER]: 'interchainGasPaymaster',
+ [HookType.AGGREGATION]: 'staticAggregationHook',
+ [HookType.PROTOCOL_FEE]: 'protocolFee',
+ [HookType.OP_STACK]: 'opStackHook',
+ [HookType.ROUTING]: 'domainRoutingHook',
+ [HookType.FALLBACK_ROUTING]: 'fallbackDomainRoutingHook',
+ [HookType.PAUSABLE]: 'pausableHook',
+ [HookType.ARB_L2_TO_L1]: 'arbL2ToL1Hook',
+};
+
export type MerkleTreeHookConfig = z.infer;
export type IgpHookConfig = z.infer;
export type ProtocolFeeHookConfig = z.infer;
diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts
index 0ec8e740b3..90bb6f06f4 100644
--- a/typescript/sdk/src/index.ts
+++ b/typescript/sdk/src/index.ts
@@ -205,7 +205,12 @@ export {
WeightedMultisigIsmConfig,
WeightedMultisigIsmConfigSchema,
} from './ism/types.js';
-export { collectValidators, moduleCanCertainlyVerify } from './ism/utils.js';
+export {
+ collectValidators,
+ moduleCanCertainlyVerify,
+ isStaticDeploymentSupported,
+ isIsmCompatible,
+} from './ism/utils.js';
export {
AgentChainMetadata,
AgentChainMetadataSchema,
diff --git a/typescript/sdk/src/ism/types.ts b/typescript/sdk/src/ism/types.ts
index 87adb2cff2..810f6ada33 100644
--- a/typescript/sdk/src/ism/types.ts
+++ b/typescript/sdk/src/ism/types.ts
@@ -67,6 +67,16 @@ export const MUTABLE_ISM_TYPE = [
IsmType.PAUSABLE,
];
+// ISM types that require static deployment
+export const STATIC_ISM_TYPES = [
+ IsmType.AGGREGATION,
+ IsmType.MERKLE_ROOT_MULTISIG,
+ IsmType.MESSAGE_ID_MULTISIG,
+ IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG,
+ IsmType.WEIGHTED_MESSAGE_ID_MULTISIG,
+ IsmType.ICA_ROUTING,
+];
+
// mapping between the two enums
export function ismTypeToModuleType(ismType: IsmType): ModuleType {
switch (ismType) {
diff --git a/typescript/sdk/src/ism/utils.ts b/typescript/sdk/src/ism/utils.ts
index 5989854ab7..d8cb457384 100644
--- a/typescript/sdk/src/ism/utils.ts
+++ b/typescript/sdk/src/ism/utils.ts
@@ -24,6 +24,7 @@ import {
import { HyperlaneContracts } from '../contracts/types.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js';
+import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainName } from '../types.js';
@@ -34,6 +35,7 @@ import {
ModuleType,
RoutingIsmConfig,
RoutingIsmDelta,
+ STATIC_ISM_TYPES,
ismTypeToModuleType,
} from './types.js';
@@ -534,3 +536,37 @@ export function collectValidators(
return new Set(validators);
}
+
+/**
+ * Determines if static ISM deployment is supported on a given chain's technical stack
+ * @dev Currently, only ZkSync does not support static deployments
+ * @param chainTechnicalStack - The technical stack of the target chain
+ * @returns boolean - true if static deployment is supported, false for ZkSync
+ */
+export function isStaticDeploymentSupported(
+ chainTechnicalStack: ChainTechnicalStack | undefined,
+): boolean {
+ if (chainTechnicalStack === undefined) return true;
+ return chainTechnicalStack !== ChainTechnicalStack.ZkSync;
+}
+
+/**
+ * Checks if the given ISM type is compatible with the chain's technical stack.
+ *
+ * @param {Object} params - The parameters object
+ * @param {ChainTechnicalStack | undefined} params.chainTechnicalStack - The technical stack of the chain
+ * @param {IsmType} params.ismType - The type of Interchain Security Module (ISM)
+ * @returns {boolean} True if the ISM type is compatible with the chain, false otherwise
+ */
+export function isIsmCompatible({
+ chainTechnicalStack,
+ ismType,
+}: {
+ chainTechnicalStack: ChainTechnicalStack | undefined;
+ ismType: IsmType;
+}): boolean {
+ // Skip compatibility check for non-static ISMs as they're always supported
+ if (!STATIC_ISM_TYPES.includes(ismType)) return true;
+
+ return isStaticDeploymentSupported(chainTechnicalStack);
+}
diff --git a/typescript/sdk/src/metadata/chainMetadataTypes.ts b/typescript/sdk/src/metadata/chainMetadataTypes.ts
index f8972f9bdb..5f57159487 100644
--- a/typescript/sdk/src/metadata/chainMetadataTypes.ts
+++ b/typescript/sdk/src/metadata/chainMetadataTypes.ts
@@ -22,6 +22,7 @@ export enum ExplorerFamily {
Etherscan = 'etherscan',
Blockscout = 'blockscout',
Routescan = 'routescan',
+ ZkSync = 'zksync',
Other = 'other',
}
diff --git a/typescript/sdk/src/utils/zksync.ts b/typescript/sdk/src/utils/zksync.ts
new file mode 100644
index 0000000000..0b97e4330c
--- /dev/null
+++ b/typescript/sdk/src/utils/zksync.ts
@@ -0,0 +1,32 @@
+import { ZKSyncArtifact, loadAllZKSyncArtifacts } from '@hyperlane-xyz/core';
+
+/**
+ * @dev Retrieves a ZkSync artifact by its contract name or qualified name.
+ * @param name The name of the contract or qualified name in the format "sourceName:contractName".
+ * @return The corresponding ZKSyncArtifact if found, or undefined if not found.
+ */
+export const getZKSyncArtifactByContractName = async (
+ name: string,
+): Promise => {
+ // Load all ZkSync artifacts
+ const allArtifacts = loadAllZKSyncArtifacts();
+
+ // Find the artifact that matches the contract name or qualified name
+ const artifact = Object.values(allArtifacts).find(
+ ({ contractName, sourceName }: ZKSyncArtifact) => {
+ const lowerCaseContractName = contractName.toLowerCase();
+ const lowerCaseName = name.toLowerCase();
+
+ // Check if the contract name matches
+ if (lowerCaseContractName === lowerCaseName) {
+ return true;
+ }
+
+ // Check if the qualified name matches
+ const qualifiedName = `${sourceName}:${contractName}`;
+ return qualifiedName === name; // Return true if qualified name matches
+ },
+ );
+
+ return artifact;
+};
diff --git a/typescript/sdk/src/zksync/ZKSyncDeployer.ts b/typescript/sdk/src/zksync/ZKSyncDeployer.ts
new file mode 100644
index 0000000000..9206926311
--- /dev/null
+++ b/typescript/sdk/src/zksync/ZKSyncDeployer.ts
@@ -0,0 +1,185 @@
+import { BigNumber, BytesLike, Overrides, utils } from 'ethers';
+import {
+ Contract,
+ ContractFactory,
+ Wallet,
+ types as zksyncTypes,
+} from 'zksync-ethers';
+
+import { ZKSyncArtifact } from '@hyperlane-xyz/core';
+import { assert } from '@hyperlane-xyz/utils';
+
+import { defaultZKProviderBuilder } from '../providers/providerBuilders.js';
+import { getZKSyncArtifactByContractName } from '../utils/zksync.js';
+
+/**
+ * Class for deploying contracts to the ZKSync network.
+ */
+export class ZKSyncDeployer {
+ public zkWallet: Wallet;
+ public deploymentType?: zksyncTypes.DeploymentType;
+
+ constructor(zkWallet: Wallet, deploymentType?: zksyncTypes.DeploymentType) {
+ this.deploymentType = deploymentType;
+
+ this.zkWallet = zkWallet.connect(
+ zkWallet.provider ??
+ defaultZKProviderBuilder([{ http: 'http://127.0.0.1:8011' }], 260),
+ );
+ }
+
+ /**
+ * Loads and validates a ZKSync contract artifact by name
+ * @param contractTitle - Contract name or qualified name (sourceName:contractName)
+ *
+ * @returns The ZKSync artifact
+ */
+ private async loadArtifact(contractTitle: string): Promise {
+ const artifact = await getZKSyncArtifactByContractName(contractTitle);
+ assert(artifact, `No ZKSync artifact for contract ${contractTitle} found!`);
+ return artifact;
+ }
+
+ /**
+ * Estimates the price of calling a deploy transaction in ETH.
+ *
+ * @param artifact The previously loaded artifact object.
+ * @param constructorArguments List of arguments to be passed to the contract constructor.
+ *
+ * @returns Calculated fee in ETH wei
+ */
+ public async estimateDeployFee(
+ artifact: ZKSyncArtifact,
+ constructorArguments: any[],
+ ): Promise {
+ const gas = await this.estimateDeployGas(artifact, constructorArguments);
+ const gasPrice = await this.zkWallet.provider.getGasPrice();
+ return gas.mul(gasPrice);
+ }
+
+ /**
+ * Estimates the amount of gas needed to execute a deploy transaction.
+ *
+ * @param artifact The previously loaded artifact object.
+ * @param constructorArguments List of arguments to be passed to the contract constructor.
+ *
+ * @returns Calculated amount of gas.
+ */
+ public async estimateDeployGas(
+ artifact: ZKSyncArtifact,
+ constructorArguments: any[],
+ ): Promise {
+ const factoryDeps = await this.extractFactoryDeps(artifact);
+
+ const factory = new ContractFactory(
+ artifact.abi,
+ artifact.bytecode,
+ this.zkWallet,
+ this.deploymentType,
+ );
+
+ // Encode deploy transaction so it can be estimated.
+ const deployTx = factory.getDeployTransaction(...constructorArguments, {
+ customData: {
+ factoryDeps,
+ },
+ });
+ deployTx.from = this.zkWallet.address;
+
+ return this.zkWallet.provider.estimateGas(deployTx);
+ }
+
+ /**
+ * Sends a deploy transaction to the zkSync network.
+ * For now, it will use defaults for the transaction parameters:
+ * - fee amount is requested automatically from the zkSync server.
+ *
+ * @param artifact The previously loaded artifact object.
+ * @param constructorArguments List of arguments to be passed to the contract constructor.
+ * @param overrides Optional object with additional deploy transaction parameters.
+ * @param additionalFactoryDeps Additional contract bytecodes to be added to the factory dependencies list.
+ *
+ * @returns A contract object.
+ */
+ public async deploy(
+ artifact: ZKSyncArtifact,
+ constructorArguments: any[] = [],
+ overrides?: Overrides,
+ additionalFactoryDeps?: BytesLike[],
+ ): Promise {
+ const baseDeps = await this.extractFactoryDeps(artifact);
+ const additionalDeps = additionalFactoryDeps
+ ? additionalFactoryDeps.map((val) => utils.hexlify(val))
+ : [];
+ const factoryDeps = [...baseDeps, ...additionalDeps];
+
+ const factory = new ContractFactory(
+ artifact.abi,
+ artifact.bytecode,
+ this.zkWallet,
+ this.deploymentType,
+ );
+
+ const { customData, ..._overrides } = overrides ?? {};
+
+ // Encode and send the deploy transaction providing factory dependencies.
+ const contract = await factory.deploy(...constructorArguments, {
+ ..._overrides,
+ customData: {
+ ...customData,
+ factoryDeps,
+ },
+ });
+
+ await contract.deployed();
+
+ return contract;
+ }
+
+ /**
+ * Extracts factory dependencies from the artifact.
+ *
+ * @param artifact Artifact to extract dependencies from
+ *
+ * @returns Factory dependencies in the format expected by SDK.
+ */
+ async extractFactoryDeps(artifact: ZKSyncArtifact): Promise {
+ const visited = new Set();
+
+ visited.add(`${artifact.sourceName}:${artifact.contractName}`);
+ return this.extractFactoryDepsRecursive(artifact, visited);
+ }
+
+ private async extractFactoryDepsRecursive(
+ artifact: ZKSyncArtifact,
+ visited: Set,
+ ): Promise {
+ // Load all the dependency bytecodes.
+ // We transform it into an array of bytecodes.
+ const factoryDeps: string[] = [];
+ for (const dependencyHash in artifact.factoryDeps) {
+ if (
+ Object.prototype.hasOwnProperty.call(
+ artifact.factoryDeps,
+ dependencyHash,
+ )
+ ) {
+ const dependencyContract = artifact.factoryDeps[dependencyHash];
+ if (!visited.has(dependencyContract)) {
+ const dependencyArtifact = await this.loadArtifact(
+ dependencyContract,
+ );
+ factoryDeps.push(dependencyArtifact.bytecode);
+ visited.add(dependencyContract);
+ const transitiveDeps = await this.extractFactoryDepsRecursive(
+ dependencyArtifact,
+ visited,
+ );
+ factoryDeps.push(...transitiveDeps);
+ }
+ }
+ }
+
+ return factoryDeps;
+ }
+}