diff --git a/.changeset/beige-suns-wave.md b/.changeset/beige-suns-wave.md new file mode 100644 index 0000000000..277f99d47c --- /dev/null +++ b/.changeset/beige-suns-wave.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Add create() with EvmCoreModule diff --git a/typescript/sdk/src/contracts/contracts.ts b/typescript/sdk/src/contracts/contracts.ts index 557e68d17d..7a58b3bfdc 100644 --- a/typescript/sdk/src/contracts/contracts.ts +++ b/typescript/sdk/src/contracts/contracts.ts @@ -146,6 +146,15 @@ export function attachContractsMapAndGetForeignDeployments< }; } +export function attachAndConnectContracts( + addresses: HyperlaneAddresses, + factories: F, + connection: Connection, +): HyperlaneContracts { + const contracts = attachContracts(addresses, factories); + return connectContracts(contracts, connection); +} + export function connectContracts( contracts: HyperlaneContracts, connection: Connection, diff --git a/typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts b/typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts new file mode 100644 index 0000000000..0d64c9080d --- /dev/null +++ b/typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts @@ -0,0 +1,158 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; +import { expect } from 'chai'; +import { constants } from 'ethers'; +import hre from 'hardhat'; + +import { + Mailbox__factory, + ProxyAdmin__factory, + TestRecipient__factory, + TimelockController__factory, + ValidatorAnnounce__factory, +} from '@hyperlane-xyz/core'; +import { objMap } from '@hyperlane-xyz/utils'; + +import { TestChainName } from '../consts/testChains.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { testCoreConfig } from '../test/testUtils.js'; + +import { EvmCoreModule } from './EvmCoreModule.js'; + +describe('EvmCoreModule', async () => { + const DELAY = 1892391283182; + let signer: SignerWithAddress; + let multiProvider: MultiProvider; + let evmCoreModule: EvmCoreModule; + let proxyAdminContract: any; + let mailboxContract: any; + let validatorAnnounceContract: any; + let testRecipientContract: any; + let timelockControllerContract: any; + + before(async () => { + [signer] = await hre.ethers.getSigners(); + multiProvider = MultiProvider.createTestMultiProvider({ signer }); + const config = { + ...testCoreConfig([TestChainName.test1])[TestChainName.test1], + upgrade: { + timelock: { + delay: DELAY, + roles: { + executor: signer.address, + proposer: signer.address, + }, + }, + }, + }; + + evmCoreModule = await EvmCoreModule.create({ + chain: TestChainName.test1, + config, + multiProvider, + }); + + const { + proxyAdmin, + mailbox, + validatorAnnounce, + testRecipient, + timelockController, + } = evmCoreModule.serialize(); + + proxyAdminContract = ProxyAdmin__factory.connect( + proxyAdmin!, + multiProvider.getProvider(TestChainName.test1), + ); + + mailboxContract = Mailbox__factory.connect( + mailbox!, + multiProvider.getProvider(TestChainName.test1), + ); + + validatorAnnounceContract = ValidatorAnnounce__factory.connect( + validatorAnnounce!, + multiProvider.getProvider(TestChainName.test1), + ); + + testRecipientContract = TestRecipient__factory.connect( + testRecipient!, + multiProvider.getProvider(TestChainName.test1), + ); + + timelockControllerContract = TimelockController__factory.connect( + timelockController!, + multiProvider.getProvider(TestChainName.test1), + ); + }); + + describe('Create', async () => { + it('should create deploy an ICA', () => { + const { interchainAccountRouter, interchainAccountIsm } = + evmCoreModule.serialize(); + expect(interchainAccountIsm).to.exist; + expect(interchainAccountRouter).to.exist; + }); + + it('should deploy ISM factories', () => { + // Each ISM factory + objMap( + evmCoreModule.serialize().ismFactoryFactories, + (_: any, factoryAddress: any) => { + expect(factoryAddress).to.exist; + expect(factoryAddress).to.not.equal(constants.AddressZero); + }, + ); + }); + + it('should deploy proxyAdmin', () => { + expect(evmCoreModule.serialize().proxyAdmin).to.exist; + }); + + it('should set proxyAdmin owner to deployer', async () => { + expect(await proxyAdminContract.owner()).to.equal(signer.address); + }); + + it('should deploy mailbox', () => { + expect(evmCoreModule.serialize().mailbox).to.exist; + }); + + it('should set mailbox owner to proxyAdmin', async () => { + expect(await mailboxContract.owner()).to.equal( + evmCoreModule.serialize().proxyAdmin, + ); + }); + + it('should deploy mailbox default Ism', async () => { + expect(await mailboxContract.defaultIsm()).to.not.equal( + constants.AddressZero, + ); + }); + + it('should deploy mailbox default hook', async () => { + expect(await mailboxContract.defaultHook()).to.not.equal( + constants.AddressZero, + ); + }); + + it('should deploy mailbox required hook', async () => { + expect(await mailboxContract.requiredHook()).to.not.equal( + constants.AddressZero, + ); + }); + + it('should deploy validatorAnnounce', async () => { + expect(evmCoreModule.serialize().validatorAnnounce).to.exist; + expect(await validatorAnnounceContract.owner()).to.equal(signer.address); + }); + + it('should deploy testRecipient', async () => { + expect(evmCoreModule.serialize().testRecipient).to.exist; + expect(await testRecipientContract.owner()).to.equal(signer.address); + }); + + it('should deploy timelock if upgrade is set', async () => { + expect(evmCoreModule.serialize().timelockController).to.exist; + expect(await timelockControllerContract.getMinDelay()).to.equal(DELAY); + }); + }); +}); diff --git a/typescript/sdk/src/core/EvmCoreModule.ts b/typescript/sdk/src/core/EvmCoreModule.ts new file mode 100644 index 0000000000..f4dd3ee10e --- /dev/null +++ b/typescript/sdk/src/core/EvmCoreModule.ts @@ -0,0 +1,276 @@ +import { Mailbox } from '@hyperlane-xyz/core'; +import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { + attachContractsMap, + serializeContractsMap, +} from '../contracts/contracts.js'; +import { HyperlaneAddresses } from '../contracts/types.js'; +import { CoreConfig } from '../core/types.js'; +import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; +import { + ProxyFactoryFactories, + proxyFactoryFactories, +} from '../deploy/contracts.js'; +import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { EthersV5Transaction } from '../providers/ProviderType.js'; +import { ChainNameOrId } from '../types.js'; + +import { + HyperlaneModule, + HyperlaneModuleArgs, +} from './AbstractHyperlaneModule.js'; +import { EvmCoreReader } from './EvmCoreReader.js'; +import { EvmIcaModule } from './EvmIcaModule.js'; +import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer.js'; +import { CoreFactories } from './contracts.js'; + +type DeployedAdresses = HyperlaneAddresses & { + testRecipient: Address; + timelockController?: Address; // Can be optional because it is only deployed if config.upgrade = true + interchainAccountRouter: Address; + interchainAccountIsm: Address; + ismFactoryFactories: HyperlaneAddresses; +}; + +export class EvmCoreModule extends HyperlaneModule< + ProtocolType.Ethereum, + CoreConfig, + DeployedAdresses +> { + protected logger = rootLogger.child({ module: 'EvmCoreModule' }); + protected coreReader: EvmCoreReader; + public readonly chainName: string; + + protected constructor( + protected readonly multiProvider: MultiProvider, + args: HyperlaneModuleArgs, + ) { + super(args); + this.coreReader = new EvmCoreReader(multiProvider, this.args.chain); + this.chainName = this.multiProvider.getChainName(this.args.chain); + } + + /** + * Reads the core configuration from the mailbox address specified in the SDK arguments. + * @returns The core config. + */ + public async read(): Promise { + return this.coreReader.deriveCoreConfig(this.args.addresses.mailbox); + } + + public async update(_config: CoreConfig): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Deploys the Core contracts. + * @remark Most of the contract owners is the Deployer with some being the Proxy Admin. + * @returns The created EvmCoreModule instance. + */ + public static async create(params: { + chain: ChainNameOrId; + config: CoreConfig; + multiProvider: MultiProvider; + }): Promise { + const { chain, config, multiProvider } = params; + const addresses = await EvmCoreModule.deploy({ + config, + multiProvider, + chain, + }); + + // 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: { + config: CoreConfig; + multiProvider: MultiProvider; + chain: ChainNameOrId; + }): Promise { + const { config, multiProvider, chain } = params; + const chainName = multiProvider.getChainName(chain); + + // Deploy Ism Factories + const ismFactoryFactories = await EvmCoreModule.deployIsmFactories({ + chainName, + config, + multiProvider, + }); + + // Deploy IsmFactory to be used in CoreDeployer + const ismFactory = new HyperlaneIsmFactory( + attachContractsMap( + { [chainName]: ismFactoryFactories }, + proxyFactoryFactories, + ), + multiProvider, + ); + + // Initalize Deployer + const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory); + + // Deploy proxyAdmin + const proxyAdmin = ( + await coreDeployer.deployContract(chainName, 'proxyAdmin', []) + ).address; + + // Deploy Mailbox + const mailbox = await this.deployMailbox({ + config, + coreDeployer, + proxyAdmin, + multiProvider, + chain, + }); + + // Deploy ICA ISM and Router + const { interchainAccountRouter, interchainAccountIsm } = ( + await EvmIcaModule.create({ + chain: chainName, + multiProvider: multiProvider, + config: { + mailbox: mailbox.address, + owner: await multiProvider.getSigner(chain).getAddress(), + }, + }) + ).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 Receipient + const testRecipient = ( + await coreDeployer.deployTestRecipient( + chainName, + await mailbox.defaultIsm(), + ) + ).address; + + // Set Core & extra addresses + return { + ismFactoryFactories, + proxyAdmin, + mailbox: mailbox.address, + interchainAccountRouter, + interchainAccountIsm, + validatorAnnounce, + timelockController, + testRecipient, + }; + } + + /** + * Deploys the ISM factories for a given chain. + * @returns The deployed ISM factories addresses. + */ + static async deployIsmFactories(params: { + chainName: string; + config: CoreConfig; + multiProvider: MultiProvider; + }): Promise> { + const { chainName, config, multiProvider } = params; + + // ChainMap is still needed for HyperlaneIsmFactory + const proxyFactoryDeployer = new HyperlaneProxyFactoryDeployer( + multiProvider, + ); + 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: { + config: CoreConfig; + proxyAdmin: Address; + coreDeployer: HyperlaneCoreDeployer; + multiProvider: MultiProvider; + chain: ChainNameOrId; + }): Promise { + 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( + proxyAdmin, + deployedDefaultIsm, + deployedDefaultHook.address, + deployedRequiredHook.address, + multiProvider.getTransactionOverrides(chain), + ), + ); + return mailbox; + } +} diff --git a/typescript/sdk/src/ism/EvmIsmModule.ts b/typescript/sdk/src/ism/EvmIsmModule.ts index 7d9921832b..c82b4194dc 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.ts @@ -1,6 +1,6 @@ import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; -import { HyperlaneContracts } from '../contracts/types.js'; +import { HyperlaneAddresses } from '../contracts/types.js'; import { HyperlaneModule, HyperlaneModuleArgs, @@ -11,79 +11,49 @@ import { MultiProvider } from '../providers/MultiProvider.js'; import { EthersV5Transaction } from '../providers/ProviderType.js'; import { ChainNameOrId } from '../types.js'; -import { EvmIsmCreator } from './EvmIsmCreator.js'; import { EvmIsmReader } from './EvmIsmReader.js'; import { IsmConfig } from './types.js'; export class EvmIsmModule extends HyperlaneModule< ProtocolType.Ethereum, IsmConfig, - HyperlaneContracts & { + HyperlaneAddresses & { deployedIsm: Address; } > { protected logger = rootLogger.child({ module: 'EvmIsmModule' }); protected reader: EvmIsmReader; - protected creator: EvmIsmCreator; protected constructor( protected readonly multiProvider: MultiProvider, protected readonly deployer: HyperlaneDeployer, args: HyperlaneModuleArgs< IsmConfig, - HyperlaneContracts & { + HyperlaneAddresses & { deployedIsm: Address; } >, ) { super(args); this.reader = new EvmIsmReader(multiProvider, args.chain); - this.creator = new EvmIsmCreator(deployer, multiProvider, args.addresses); } public async read(): Promise { return await this.reader.deriveIsmConfig(this.args.addresses.deployedIsm); } - public async update(config: IsmConfig): Promise { + public async update(_config: IsmConfig): Promise { throw new Error('Method not implemented.'); - - const destination = this.multiProvider.getChainName(this.args.chain); - await this.creator.update({ - destination, - config, - existingIsmAddress: this.args.addresses.deployedIsm, - }); - return []; } // manually write static create function - public static async create({ - chain, - config, - deployer, - factories, - multiProvider, - }: { + public static async create(_params: { chain: ChainNameOrId; config: IsmConfig; deployer: HyperlaneDeployer; - factories: HyperlaneContracts; + factories: HyperlaneAddresses; multiProvider: MultiProvider; }): Promise { - const destination = multiProvider.getChainName(chain); - const ismCreator = new EvmIsmCreator(deployer, multiProvider, factories); - const deployedIsm = await ismCreator.deploy({ - config, - destination, - }); - return new EvmIsmModule(multiProvider, deployer, { - addresses: { - ...factories, - deployedIsm: deployedIsm.address, - }, - chain, - config, - }); + throw new Error('Method not implemented.'); } }