@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
313 lines • 15.7 kB
JavaScript
import { Mailbox__factory, Ownable__factory, } from '@hyperlane-xyz/core';
import { eqAddress, rootLogger, } from '@hyperlane-xyz/utils';
import { attachContractsMap, serializeContractsMap, transferOwnershipTransactions, } from '../contracts/contracts.js';
import { CoreConfigSchema, } from '../core/types.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { proxyFactoryFactories, } from '../deploy/contracts.js';
import { proxyAdminUpdateTxs } from '../deploy/proxy.js';
import { createDefaultProxyFactoryFactories } from '../deploy/proxyFactoryUtils.js';
import { getEvmHookUpdateTransactions } from '../hook/updates.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { IsmType } from '../ism/types.js';
import { isStaticDeploymentSupported } from '../ism/utils.js';
import { extractIsmAndHookFactoryAddresses } from '../utils/ism.js';
import { HyperlaneModule, } from './AbstractHyperlaneModule.js';
import { EvmCoreReader } from './EvmCoreReader.js';
import { EvmIcaModule } from './EvmIcaModule.js';
import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer.js';
export class EvmCoreModule extends HyperlaneModule {
multiProvider;
logger = rootLogger.child({ module: 'EvmCoreModule' });
coreReader;
evmIcaModule;
chainName;
chainId;
domainId;
constructor(multiProvider, args) {
super(args);
this.multiProvider = multiProvider;
this.coreReader = new EvmCoreReader(multiProvider, args.chain);
this.chainName = multiProvider.getChainName(args.chain);
this.chainId = multiProvider.getEvmChainId(args.chain);
this.domainId = multiProvider.getDomainId(args.chain);
if (args.config.interchainAccountRouter) {
this.evmIcaModule = new EvmIcaModule(multiProvider, {
chain: args.chain,
addresses: {
interchainAccountRouter: args.addresses.interchainAccountRouter,
},
config: args.config.interchainAccountRouter,
});
}
}
/**
* Reads the core configuration from the mailbox address specified in the SDK arguments.
* @returns The core config.
*/
async read() {
return this.coreReader.deriveCoreConfig({
mailbox: this.args.addresses.mailbox,
interchainAccountRouter: this.args.addresses.interchainAccountRouter,
});
}
/**
* Updates the core contracts with the provided configuration.
*
* @param expectedConfig - The configuration for the core contracts to be updated.
* @returns An array of Ethereum transactions that were executed to update the contract.
*/
async update(expectedConfig) {
CoreConfigSchema.parse(expectedConfig);
const actualConfig = await this.read();
const transactions = [
...(await this.createDefaultIsmUpdateTxs(actualConfig, expectedConfig)),
];
const proxyAdminAddress = expectedConfig.proxyAdmin?.address ??
actualConfig.proxyAdmin?.address ??
this.args.addresses.proxyAdmin;
if (expectedConfig.requiredHook) {
transactions.push(...(await this.createHookUpdateTxs(proxyAdminAddress, 'requiredHook', actualConfig.requiredHook, expectedConfig.requiredHook)));
}
if (expectedConfig.defaultHook) {
transactions.push(...(await this.createHookUpdateTxs(proxyAdminAddress, 'defaultHook', actualConfig.defaultHook, expectedConfig.defaultHook)));
}
if (expectedConfig.interchainAccountRouter && this.evmIcaModule) {
transactions.push(...(await this.evmIcaModule.update(expectedConfig.interchainAccountRouter)));
}
transactions.push(...this.createMailboxOwnerUpdateTxs(actualConfig, expectedConfig), ...proxyAdminUpdateTxs(this.chainId, this.args.addresses.mailbox, actualConfig, expectedConfig));
return transactions;
}
async createHookUpdateTxs(proxyAdminAddress, setHookFunctionName, actualConfig, expectedConfig) {
return getEvmHookUpdateTransactions(this.args.addresses.mailbox, {
actualConfig: actualConfig,
expectedConfig: expectedConfig,
evmChainName: this.chainName,
hookAndIsmFactories: extractIsmAndHookFactoryAddresses(this.args.addresses),
setHookFunctionCallEncoder: (newHookAddress) => {
if (setHookFunctionName === 'requiredHook') {
return Mailbox__factory.createInterface().encodeFunctionData('setRequiredHook', [newHookAddress]);
}
return Mailbox__factory.createInterface().encodeFunctionData('setDefaultHook', [newHookAddress]);
},
logger: this.logger,
mailbox: this.args.addresses.mailbox,
multiProvider: this.multiProvider,
proxyAdminAddress,
});
}
/**
* Create a transaction to update an existing ISM config, or deploy a new ISM and return a tx to setDefaultIsm
*
* @param actualConfig - The on-chain router configuration, including the ISM configuration, and address.
* @param expectedConfig - The expected token router configuration, including the ISM configuration.
* @returns Transaction that need to be executed to update the ISM configuration.
*/
async createDefaultIsmUpdateTxs(actualConfig, expectedConfig) {
const updateTransactions = [];
const actualDefaultIsmConfig = actualConfig.defaultIsm;
// Try to update (may also deploy) Ism with the expected config
const { deployedIsm, ismUpdateTxs } = await this.deployOrUpdateIsm(actualDefaultIsmConfig, expectedConfig.defaultIsm);
if (ismUpdateTxs.length) {
updateTransactions.push(...ismUpdateTxs);
}
const newIsmDeployed = !eqAddress(actualDefaultIsmConfig.address, deployedIsm);
if (newIsmDeployed) {
const { mailbox } = this.serialize();
const contractToUpdate = Mailbox__factory.connect(mailbox, this.multiProvider.getProvider(this.domainId));
updateTransactions.push({
annotation: `Setting default ISM for Mailbox ${mailbox} to ${deployedIsm}`,
chainId: this.chainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData('setDefaultIsm', [
deployedIsm,
]),
});
}
return updateTransactions;
}
/**
* Updates or deploys the ISM using the provided configuration.
*
* @returns Object with deployedIsm address, and update Transactions
*/
async deployOrUpdateIsm(actualDefaultIsmConfig, expectDefaultIsmConfig) {
const { mailbox } = this.serialize();
const ismModule = new EvmIsmModule(this.multiProvider, {
chain: this.args.chain,
config: expectDefaultIsmConfig,
addresses: {
mailbox,
...extractIsmAndHookFactoryAddresses(this.serialize()),
deployedIsm: actualDefaultIsmConfig.address,
},
});
this.logger.info(`Comparing target ISM config with ${this.args.chain} chain`);
const ismUpdateTxs = await ismModule.update(expectDefaultIsmConfig);
const { deployedIsm } = ismModule.serialize();
return { deployedIsm, ismUpdateTxs };
}
/**
* Create a transaction to transfer ownership of an existing mailbox with a given config.
*
* @param actualConfig - The on-chain core configuration.
* @param expectedConfig - The expected token core configuration.
* @returns Ethereum transaction that need to be executed to update the owner.
*/
createMailboxOwnerUpdateTxs(actualConfig, expectedConfig) {
return transferOwnershipTransactions(this.chainId, this.args.addresses.mailbox, actualConfig, expectedConfig, 'Mailbox');
}
/**
* Deploys the Core contracts.
* @remark Most of the contract owners is the Deployer with some being the Proxy Admin.
* @returns The created EvmCoreModule instance.
*/
static async create(params) {
const { chain, config, multiProvider, contractVerifier } = params;
const addresses = await EvmCoreModule.deploy({
config,
multiProvider,
chain,
contractVerifier,
});
// Create CoreModule and deploy the Core contracts
const module = new EvmCoreModule(multiProvider, {
addresses,
chain,
config,
});
return module;
}
/**
* Deploys the core Hyperlane contracts.
* @returns The deployed core contract addresses.
*/
static async deploy(params) {
const { config, multiProvider, chain, contractVerifier } = params;
const { name: chainName, technicalStack } = multiProvider.getChainMetadata(chain);
const ismFactoryFactories = await this.getIsmFactoryFactories(technicalStack, {
chainName,
config,
multiProvider,
contractVerifier,
});
const ismFactory = new HyperlaneIsmFactory(attachContractsMap({ [chainName]: ismFactoryFactories }, proxyFactoryFactories), multiProvider);
const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory, contractVerifier);
// Deploy proxyAdmin
const proxyAdmin = await coreDeployer.deployContract(chainName, 'proxyAdmin', []);
// Deploy Mailbox
const mailbox = await this.deployMailbox({
config,
coreDeployer,
proxyAdmin: proxyAdmin.address,
multiProvider,
chain,
});
// Deploy ICA ISM and Router
const { interchainAccountRouter } = (await EvmIcaModule.create({
chain: chainName,
multiProvider: multiProvider,
config: {
mailbox: mailbox.address,
owner: await multiProvider.getSigner(chain).getAddress(),
commitmentIsm: {
type: IsmType.OFFCHAIN_LOOKUP,
urls: ['https://commitment-read-ism.hyperlane.xyz'],
owner: await multiProvider.getSigner(chain).getAddress(),
},
},
contractVerifier,
})).serialize();
// Deploy Validator announce
const validatorAnnounce = (await coreDeployer.deployValidatorAnnounce(chainName, mailbox.address)).address;
// Deploy timelock controller if config.upgrade is set
let timelockController;
if (config.upgrade) {
timelockController = (await coreDeployer.deployTimelock(chainName, config.upgrade.timelock)).address;
}
// Deploy Test Recipient
const testRecipient = (await coreDeployer.deployTestRecipient(chainName, await mailbox.defaultIsm())).address;
// Obtain addresses of every contract created by the deployer
// and extract only the merkleTreeHook and interchainGasPaymaster
const serializedContracts = serializeContractsMap(coreDeployer.deployedContracts);
const { merkleTreeHook, interchainGasPaymaster } = serializedContracts[chainName];
// Update the ProxyAdmin owner of the Mailbox if the config defines a different owner from the current signer
const currentProxyOwner = await proxyAdmin.owner();
if (config?.proxyAdmin?.owner &&
!eqAddress(config.proxyAdmin.owner, currentProxyOwner)) {
await multiProvider.sendTransaction(chainName, {
annotation: `Transferring ownership of ProxyAdmin to the configured address ${config.proxyAdmin.owner}`,
to: proxyAdmin.address,
data: Ownable__factory.createInterface().encodeFunctionData('transferOwnership(address)', [config.proxyAdmin.owner]),
});
}
// Set Core & extra addresses
return {
...ismFactoryFactories,
proxyAdmin: proxyAdmin.address,
mailbox: mailbox.address,
interchainAccountRouter,
validatorAnnounce,
timelockController,
testRecipient,
merkleTreeHook,
interchainGasPaymaster,
};
}
/**
* Deploys the ISM factories for a given chain.
* @returns The deployed ISM factories addresses.
*/
static async deployIsmFactories(params) {
const { chainName, config, multiProvider, contractVerifier } = params;
const proxyFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider, contractVerifier);
const ismFactoriesFactory = await proxyFactoryDeployer.deploy({
[chainName]: config,
});
return serializeContractsMap(ismFactoriesFactory)[chainName];
}
/**
* Deploys a Mailbox and its default ISM, hook, and required hook contracts with a given configuration.
* @returns The deployed Mailbox contract instance.
*/
static async deployMailbox(params) {
const { config, proxyAdmin, coreDeployer: deployer, multiProvider, chain, } = params;
const chainName = multiProvider.getChainName(chain);
const domain = multiProvider.getDomainId(chainName);
const mailbox = await deployer.deployProxiedContract(chainName, 'mailbox', 'mailbox', proxyAdmin, [domain]);
// @todo refactor when 1) IsmModule is ready
const deployedDefaultIsm = await deployer.deployIsm(chainName, config.defaultIsm, mailbox.address);
// @todo refactor when 1) HookModule is ready, and 2) Hooks Config can handle strings
const deployedDefaultHook = await deployer.deployHook(chainName, config.defaultHook, {
mailbox: mailbox.address,
proxyAdmin,
});
// @todo refactor when 1) HookModule is ready, and 2) Hooks Config can handle strings
const deployedRequiredHook = await deployer.deployHook(chainName, config.requiredHook, {
mailbox: mailbox.address,
proxyAdmin,
});
// Initialize Mailbox
await multiProvider.handleTx(chain, mailbox.initialize(config.owner, deployedDefaultIsm, deployedDefaultHook.address, deployedRequiredHook.address, multiProvider.getTransactionOverrides(chain)));
return mailbox;
}
/**
* Retrieves the ISM factory factories based on the provided technicalStack and parameters.
*
* @param technicalStack - The technicalStack to determine if static address set deployment should be skipped.
* @param params - An object containing the parameters needed for ISM factory deployment.
* @param params.chainName - The name of the chain for which the ISM factories are being deployed.
* @param params.config - The core configuration to be used during deployment.
* @param params.multiProvider - The multi-provider instance for interacting with the blockchain.
* @param params.contractVerifier - An optional contract verifier for validating contracts during deployment.
* @returns A promise that resolves to the addresses of the deployed ISM factory factories.
*/
static async getIsmFactoryFactories(technicalStack, params) {
// Check if we should skip static address set deployment
if (!isStaticDeploymentSupported(technicalStack)) {
return createDefaultProxyFactoryFactories();
}
return EvmCoreModule.deployIsmFactories(params);
}
}
//# sourceMappingURL=EvmCoreModule.js.map