@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
415 lines • 22.3 kB
JavaScript
import { ethers } from 'ethers';
import { ProxyAdmin__factory, TimelockController__factory, TransparentUpgradeableProxy__factory, } from '@hyperlane-xyz/core';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import { ProtocolType, addBufferToGasLimit, eqAddress, isZeroishAddress, rootLogger, runWithTimeout, } from '@hyperlane-xyz/utils';
import { ExplorerLicenseType } from '../block-explorer/etherscan.js';
import { moduleMatchesConfig } from '../ism/utils.js';
import { ChainTechnicalStack, ExplorerFamily, } from '../metadata/chainMetadataTypes.js';
import { InterchainAccount } from '../middleware/account/InterchainAccount.js';
import { getZKSyncArtifactByContractName } from '../utils/zksync.js';
import { isInitialized, isProxy, proxyAdmin, proxyConstructorArgs, proxyImplementation, } from './proxy.js';
import { ContractVerifier } from './verify/ContractVerifier.js';
import { ZKSyncContractVerifier } from './verify/ZKSyncContractVerifier.js';
import { buildVerificationInput, getContractVerificationInput, getContractVerificationInputForZKSync, shouldAddVerificationInput, } from './verify/utils.js';
export class HyperlaneDeployer {
multiProvider;
factories;
options;
recoverVerificationInputs;
icaAddresses;
verificationInputs = {};
cachedAddresses = {};
deployedContracts = {};
cachingEnabled = true;
logger;
chainTimeoutMs;
zkSyncContractVerifier;
constructor(multiProvider, factories, options = {}, recoverVerificationInputs = false, icaAddresses = {}) {
this.multiProvider = multiProvider;
this.factories = factories;
this.options = options;
this.recoverVerificationInputs = recoverVerificationInputs;
this.icaAddresses = icaAddresses;
this.logger = options?.logger ?? rootLogger.child({ module: 'deployer' });
this.chainTimeoutMs = options?.chainTimeoutMs ?? 15 * 60 * 1000; // 15 minute timeout per chain
if (Object.keys(icaAddresses).length > 0) {
this.options.icaApp = InterchainAccount.fromAddressesMap(icaAddresses, multiProvider);
}
// if none provided, instantiate a default verifier with the default core contract build artifact
this.options.contractVerifier ??= new ContractVerifier(multiProvider, {}, coreBuildArtifact, ExplorerLicenseType.MIT);
this.zkSyncContractVerifier = new ZKSyncContractVerifier(multiProvider);
}
cacheAddressesMap(addressesMap) {
this.cachedAddresses = addressesMap;
}
async verifyContract(chain, input, logger = this.logger) {
const explorerFamily = this.multiProvider.tryGetExplorerApi(chain)?.family;
const verifier = explorerFamily === ExplorerFamily.ZkSync
? this.zkSyncContractVerifier
: this.options.contractVerifier;
return verifier?.verifyContract(chain, input, logger);
}
async deploy(configMap) {
const configChains = Object.keys(configMap);
const ethereumConfigChains = configChains.filter((chain) => this.multiProvider.getChainMetadata(chain).protocol ===
ProtocolType.Ethereum);
const targetChains = this.multiProvider.intersect(ethereumConfigChains, true).intersection;
this.logger.debug(`Start deploy to ${targetChains}`);
const failedChains = [];
const deployChain = async (chain) => {
const signerUrl = await this.multiProvider.tryGetExplorerAddressUrl(chain);
const signerAddress = await this.multiProvider.getSignerAddress(chain);
const fromString = signerUrl || signerAddress;
this.logger.info(`Deploying to ${chain} from ${fromString}`);
return runWithTimeout(this.chainTimeoutMs, async () => {
const contracts = await this.deployContracts(chain, configMap[chain]);
this.addDeployedContracts(chain, contracts);
})
.then(() => {
this.logger.info(`Successfully deployed contracts on ${chain}`);
})
.catch((error) => {
failedChains.push(chain);
this.logger.error(`Deployment failed on ${chain}. Error: ${error}`);
throw error;
});
};
if (this.options.concurrentDeploy) {
await Promise.allSettled(targetChains.map(deployChain));
}
else {
for (const chain of targetChains) {
await deployChain(chain);
}
}
if (failedChains.length > 0) {
throw new Error(`Deployment failed on chains: ${failedChains.join(', ')}`);
}
return this.deployedContracts;
}
addDeployedContracts(chain, contracts, verificationInputs) {
this.deployedContracts[chain] = {
...this.deployedContracts[chain],
...contracts,
};
if (verificationInputs)
this.addVerificationArtifacts(chain, verificationInputs);
}
addVerificationArtifacts(chain, artifacts) {
this.verificationInputs[chain] = this.verificationInputs[chain] || [];
artifacts.forEach((artifact) => {
if (shouldAddVerificationInput(this.verificationInputs, chain, artifact)) {
this.verificationInputs[chain].push(artifact);
}
});
}
async runIf(chain, address, fn, label = 'address') {
const signer = await this.multiProvider.getSignerAddress(chain);
if (eqAddress(address, signer)) {
return fn();
}
else {
this.logger.debug(`Signer (${signer}) does not match ${label} (${address})`);
}
return undefined;
}
async runIfOwner(chain, ownable, fn) {
return this.runIf(chain, await ownable.callStatic.owner(), fn, 'owner');
}
async runIfAdmin(chain, proxy, signerAdminFn, proxyAdminOwnerFn) {
const admin = await proxyAdmin(this.multiProvider.getProvider(chain), proxy.address);
const code = await this.multiProvider.getProvider(chain).getCode(admin);
// if admin is a ProxyAdmin, run the proxyAdminOwnerFn (if deployer is owner)
if (code !== '0x') {
this.logger.debug(`Admin is a ProxyAdmin (${admin})`);
const proxyAdmin = ProxyAdmin__factory.connect(admin, proxy.signer);
return this.runIfOwner(chain, proxyAdmin, () => proxyAdminOwnerFn(proxyAdmin));
}
else {
this.logger.debug(`Admin is an EOA (${admin})`);
// if admin is an EOA, run the signerAdminFn (if deployer is admin)
return this.runIf(chain, admin, () => signerAdminFn(), 'admin');
}
}
async configureIsm(chain, contract, config, getIsm, setIsm) {
const configuredIsm = await getIsm(contract);
let matches = false;
let targetIsm;
if (typeof config === 'string') {
if (eqAddress(configuredIsm, config)) {
matches = true;
}
else {
targetIsm = config;
}
}
else {
const ismFactory = this.options.ismFactory ??
(() => {
throw new Error('No ISM factory provided');
})();
matches = await moduleMatchesConfig(chain, configuredIsm, config, this.multiProvider, ismFactory.getContracts(chain));
targetIsm = (await ismFactory.deploy({ destination: chain, config }))
.address;
}
if (!matches) {
await this.runIfOwner(chain, contract, async () => {
this.logger.debug(`Set ISM on ${chain} with address ${targetIsm}`);
const populatedTx = await setIsm(contract, targetIsm);
const estimatedGas = await this.multiProvider
.getSigner(chain)
.estimateGas(populatedTx);
populatedTx.gasLimit = addBufferToGasLimit(estimatedGas);
await this.multiProvider.sendTransaction(chain, populatedTx);
if (!eqAddress(targetIsm, await getIsm(contract))) {
throw new Error(`Set ISM failed on ${chain}`);
}
});
}
}
async configureHook(chain, contract, config, getHook, setHook) {
if (typeof config !== 'string') {
throw new Error('Legacy deployer does not support hook objects');
}
const configuredHook = await getHook(contract);
if (!eqAddress(config, configuredHook)) {
await this.runIfOwner(chain, contract, async () => {
this.logger.debug(`Set hook on ${chain} to ${config}, currently is ${configuredHook}`);
await this.multiProvider.sendTransaction(chain, setHook(contract, config));
const actualHook = await getHook(contract);
if (!eqAddress(config, actualHook)) {
throw new Error(`Set hook failed on ${chain}, wanted ${config}, got ${actualHook}`);
}
});
}
}
async configureClient(local, client, config) {
this.logger.debug(`Initializing mailbox client (if not already) on ${local}...`);
if (config.hook) {
await this.configureHook(local, client, config.hook, (_client) => _client.hook(), (_client, _hook) => _client.populateTransaction.setHook(_hook));
}
if (config.interchainSecurityModule) {
await this.configureIsm(local, client, config.interchainSecurityModule, (_client) => _client.interchainSecurityModule(), (_client, _module) => _client.populateTransaction.setInterchainSecurityModule(_module));
}
this.logger.debug(`Mailbox client on ${local} initialized...`);
}
initializeFnSignature(_contractName) {
return 'initialize';
}
async deployContractFromFactory(chain, factory, contractName, constructorArgs, initializeArgs, shouldRecover = true, implementationAddress) {
if (this.cachingEnabled && shouldRecover) {
const cachedContract = this.readCache(chain, factory, contractName);
if (cachedContract) {
if (this.recoverVerificationInputs) {
const recoveredInputs = await this.recoverVerificationArtifacts(chain, contractName, cachedContract, constructorArgs, initializeArgs);
this.addVerificationArtifacts(chain, recoveredInputs);
}
return cachedContract;
}
}
this.logger.info(`Deploying ${contractName} on ${chain} with constructor args (${constructorArgs.join(', ')})...`);
const { technicalStack } = this.multiProvider.getChainMetadata(chain);
const isZKSyncChain = technicalStack === ChainTechnicalStack.ZkSync;
const signer = this.multiProvider.getSigner(chain);
const artifact = await getZKSyncArtifactByContractName(contractName);
const contract = await this.multiProvider.handleDeploy(chain, factory, constructorArgs, artifact);
if (initializeArgs) {
if (await isInitialized(this.multiProvider.getProvider(chain), contract.address)) {
this.logger.debug(`Skipping: Contract ${contractName} (${contract.address}) on ${chain} is already initialized`);
}
else {
this.logger.debug(`Initializing ${contractName} (${contract.address}) on ${chain}...`);
// Estimate gas for the initialize transaction
const estimatedGas = await contract
.connect(signer)
.estimateGas[this.initializeFnSignature(contractName)](...initializeArgs);
// deploy with buffer on gas limit
const overrides = this.multiProvider.getTransactionOverrides(chain);
const initTx = await contract[this.initializeFnSignature(contractName)](...initializeArgs, {
gasLimit: addBufferToGasLimit(estimatedGas),
...overrides,
});
const receipt = await this.multiProvider.handleTx(chain, initTx);
this.logger.debug(`Successfully initialized ${contractName} (${contract.address}) on ${chain}: ${receipt.transactionHash}`);
}
}
let verificationInput;
if (isZKSyncChain) {
if (!artifact) {
throw new Error(`No ZkSync artifact found for contract: ${contractName}`);
}
verificationInput = await getContractVerificationInputForZKSync({
name: contractName,
contract,
constructorArgs: constructorArgs,
artifact: artifact,
expectedimplementation: implementationAddress,
});
}
else {
verificationInput = getContractVerificationInput({
name: contractName,
contract,
bytecode: factory.bytecode,
expectedimplementation: implementationAddress,
});
}
this.addVerificationArtifacts(chain, [verificationInput]);
// try verifying contract
try {
await this.verifyContract(chain, verificationInput);
}
catch (error) {
// log error but keep deploying, can also verify post-deployment if needed
this.logger.debug(`Error verifying contract: ${error}`);
}
return contract;
}
/**
* Deploys a contract with a specified name.
*
* This is a generic function capable of deploying any contract type, defined within the `Factories` type, to a specified chain.
*
* @param {ChainName} chain - The name of the chain on which the contract is to be deployed.
* @param {K} contractKey - The key identifying the factory to use for deployment.
* @param {string} contractName - The name of the contract to deploy. This must match the contract source code.
* @param {Parameters<Factories[K]['deploy']>} constructorArgs - Arguments for the contract's constructor.
* @param {Parameters<Awaited<ReturnType<Factories[K]['deploy']>>['initialize']>?} initializeArgs - Optional arguments for the contract's initialization function.
* @param {boolean} shouldRecover - Flag indicating whether to attempt recovery if deployment fails.
* @returns {Promise<HyperlaneContracts<Factories>[K]>} A promise that resolves to the deployed contract instance.
*/
async deployContractWithName(chain, contractKey, contractName, constructorArgs, initializeArgs, shouldRecover = true) {
const contract = await this.deployContractFromFactory(chain, this.factories[contractKey], contractName, constructorArgs, initializeArgs, shouldRecover);
this.writeCache(chain, contractName, contract.address);
return contract;
}
async deployContract(chain, contractKey, constructorArgs, initializeArgs, shouldRecover = true) {
return this.deployContractWithName(chain, contractKey, contractKey.toString(), constructorArgs, initializeArgs, shouldRecover);
}
async changeAdmin(chain, proxy, admin) {
const actualAdmin = await proxyAdmin(this.multiProvider.getProvider(chain), proxy.address);
if (eqAddress(admin, actualAdmin)) {
this.logger.debug(`Admin set correctly, skipping admin change`);
return;
}
const txOverrides = this.multiProvider.getTransactionOverrides(chain);
this.logger.debug(`Changing proxy admin`);
await this.runIfAdmin(chain, proxy, () => this.multiProvider.handleTx(chain, proxy.changeAdmin(admin, txOverrides)), (proxyAdmin) => this.multiProvider.handleTx(chain, proxyAdmin.changeProxyAdmin(proxy.address, admin, txOverrides)));
}
async upgradeAndInitialize(chain, proxy, implementation, initializeArgs) {
const current = await proxy.callStatic.implementation();
if (eqAddress(implementation.address, current)) {
this.logger.debug(`Implementation set correctly, skipping upgrade`);
return;
}
this.logger.debug(`Upgrading and initializing implementation`);
const initData = implementation.interface.encodeFunctionData('initialize', initializeArgs);
const overrides = this.multiProvider.getTransactionOverrides(chain);
await this.runIfAdmin(chain, proxy, () => this.multiProvider.handleTx(chain, proxy.upgradeToAndCall(implementation.address, initData, overrides)), (proxyAdmin) => this.multiProvider.handleTx(chain, proxyAdmin.upgradeAndCall(proxy.address, implementation.address, initData, overrides)));
}
async deployProxy(chain, implementation, proxyAdmin, initializeArgs, contractName) {
const isProxied = await isProxy(this.multiProvider.getProvider(chain), implementation.address);
if (isProxied) {
// if the implementation is already a proxy, do not deploy a new proxy
return implementation;
}
const constructorArgs = proxyConstructorArgs(implementation, proxyAdmin, initializeArgs, this.initializeFnSignature(contractName ?? ''));
const proxy = await this.deployContractFromFactory(chain, new TransparentUpgradeableProxy__factory(), 'TransparentUpgradeableProxy', constructorArgs, undefined, true, implementation.address);
return implementation.attach(proxy.address);
}
async deployTimelock(chain, timelockConfig) {
const TimelockZkArtifact = await getZKSyncArtifactByContractName('TimelockController');
return this.multiProvider.handleDeploy(chain, new TimelockController__factory(),
// delay, [proposers], [executors], admin
[
timelockConfig.delay,
[timelockConfig.roles.proposer],
[timelockConfig.roles.executor],
ethers.constants.AddressZero,
], TimelockZkArtifact);
}
writeCache(chain, contractName, address) {
if (!this.cachedAddresses[chain]) {
this.cachedAddresses[chain] = {};
}
this.cachedAddresses[chain][contractName] = address;
}
readCache(chain, factory, contractName) {
const cachedAddress = this.cachedAddresses[chain]?.[contractName];
if (cachedAddress && !isZeroishAddress(cachedAddress)) {
this.logger.debug(`Recovered ${contractName} on ${chain}: ${cachedAddress}`);
return factory
.attach(cachedAddress)
.connect(this.multiProvider.getSignerOrProvider(chain));
}
return undefined;
}
async recoverVerificationArtifacts(chain, contractName, cachedContract, constructorArgs, initializeArgs) {
const provider = this.multiProvider.getProvider(chain);
const isProxied = await isProxy(provider, cachedContract.address);
let implementation;
if (isProxied) {
implementation = await proxyImplementation(provider, cachedContract.address);
}
else {
implementation = cachedContract.address;
}
const implementationInput = buildVerificationInput(contractName, implementation, cachedContract.interface.encodeDeploy(constructorArgs));
if (!isProxied) {
return [implementationInput];
}
const admin = await proxyAdmin(provider, cachedContract.address);
const proxyArgs = proxyConstructorArgs(cachedContract.attach(implementation), admin, initializeArgs, contractName);
const proxyInput = buildVerificationInput('TransparentUpgradeableProxy', cachedContract.address, TransparentUpgradeableProxy__factory.createInterface().encodeDeploy(proxyArgs));
return [implementationInput, proxyInput];
}
/**
* Deploys the Implementation and Proxy for a given contract
*
*/
async deployProxiedContract(chain, contractKey, contractName, proxyAdmin, constructorArgs, initializeArgs) {
// Try to initialize the implementation even though it may not be necessary
const implementation = await this.deployContractWithName(chain, contractKey, contractName, constructorArgs, initializeArgs);
// Initialize the proxy the same way
const contract = await this.deployProxy(chain, implementation, proxyAdmin, initializeArgs, contractName);
this.writeCache(chain, contractName, contract.address);
return contract;
}
mergeWithExistingVerificationInputs(existingInputsMap) {
const allChains = new Set();
Object.keys(existingInputsMap).forEach((_) => allChains.add(_));
Object.keys(this.verificationInputs).forEach((_) => allChains.add(_));
const ret = {};
for (const chain of allChains) {
const existingInputs = existingInputsMap[chain] || [];
const newInputs = this.verificationInputs[chain] || [];
ret[chain] = [...existingInputs, ...newInputs];
}
return ret;
}
async transferOwnershipOfContracts(chain, config, ownables) {
const receipts = [];
for (const [contractName, ownable] of Object.entries(ownables)) {
if (!ownable) {
continue;
}
const current = await ownable.owner();
const owner = config.ownerOverrides?.[contractName] ?? config.owner;
if (!eqAddress(current, owner)) {
this.logger.debug({ contractName, current, desiredOwner: owner }, 'Current owner and config owner do not match');
const receipt = await this.runIfOwner(chain, ownable, async () => {
this.logger.debug(`Transferring ownership of ${contractName} to ${owner} on ${chain}`);
const estimatedGas = await ownable.estimateGas.transferOwnership(owner);
return this.multiProvider.handleTx(chain, ownable.transferOwnership(owner, {
gasLimit: addBufferToGasLimit(estimatedGas),
...this.multiProvider.getTransactionOverrides(chain),
}));
});
if (receipt)
receipts.push(receipt);
}
}
return receipts.filter((x) => !!x);
}
}
//# sourceMappingURL=HyperlaneDeployer.js.map