From e104cf6aa31bffc1bf026140cfaa5f6e50febf75 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 28 Oct 2024 13:28:12 -0400 Subject: [PATCH] refactor: dedupe HyperlaneIsmFactory and IsmModule.create (#4732) ### Description - Uses HyperlaneIsmFactory in IsmModuleCreate for deduping redundant code ### Backward compatibility Yes ### Testing Unit tests --- .changeset/dirty-swans-drum.md | 6 + typescript/sdk/src/contracts/contracts.ts | 31 +- typescript/sdk/src/core/EvmCoreModule.ts | 15 +- .../sdk/src/deploy/EvmModuleDeployer.ts | 359 --------------- .../sdk/src/deploy/HyperlaneDeployer.ts | 5 +- typescript/sdk/src/hook/EvmHookModule.ts | 150 +++---- typescript/sdk/src/ism/EvmIsmModule.ts | 417 ++---------------- .../ism/HyperlaneIsmFactory.hardhat-test.ts | 1 - typescript/sdk/src/ism/HyperlaneIsmFactory.ts | 85 ++-- .../sdk/src/token/EvmERC20WarpModule.ts | 15 +- typescript/utils/src/index.ts | 11 +- typescript/utils/src/sets.ts | 10 + 12 files changed, 237 insertions(+), 868 deletions(-) create mode 100644 .changeset/dirty-swans-drum.md delete mode 100644 typescript/sdk/src/deploy/EvmModuleDeployer.ts diff --git a/.changeset/dirty-swans-drum.md b/.changeset/dirty-swans-drum.md new file mode 100644 index 000000000..963f91b39 --- /dev/null +++ b/.changeset/dirty-swans-drum.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/utils': patch +'@hyperlane-xyz/sdk': patch +--- + +Dedupe internals of hook and ISM module deploy code diff --git a/typescript/sdk/src/contracts/contracts.ts b/typescript/sdk/src/contracts/contracts.ts index 5f1913a55..aadeba085 100644 --- a/typescript/sdk/src/contracts/contracts.ts +++ b/typescript/sdk/src/contracts/contracts.ts @@ -1,10 +1,11 @@ import { Contract } from 'ethers'; -import { Ownable } from '@hyperlane-xyz/core'; +import { Ownable, Ownable__factory } from '@hyperlane-xyz/core'; import { Address, ProtocolType, ValueOf, + eqAddress, hexOrBase58ToHex, objFilter, objMap, @@ -12,8 +13,10 @@ import { promiseObjAll, } from '@hyperlane-xyz/utils'; +import { OwnableConfig } from '../deploy/types.js'; import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; import { MultiProvider } from '../providers/MultiProvider.js'; +import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { ChainMap, Connection } from '../types.js'; import { @@ -257,3 +260,29 @@ export function appFromAddressesMapHelper( multiProvider, }; } + +export function transferOwnershipTransactions( + chainId: number, + contract: Address, + actual: OwnableConfig, + expected: OwnableConfig, + label?: string, +): AnnotatedEV5Transaction[] { + if (eqAddress(actual.owner, expected.owner)) { + return []; + } + + return [ + { + chainId, + annotation: `Transferring ownership of ${label ?? contract} from ${ + actual.owner + } to ${expected.owner}`, + to: contract, + data: Ownable__factory.createInterface().encodeFunctionData( + 'transferOwnership', + [expected.owner], + ), + }, + ]; +} diff --git a/typescript/sdk/src/core/EvmCoreModule.ts b/typescript/sdk/src/core/EvmCoreModule.ts index fd4e96018..1be81d98f 100644 --- a/typescript/sdk/src/core/EvmCoreModule.ts +++ b/typescript/sdk/src/core/EvmCoreModule.ts @@ -10,6 +10,7 @@ import { import { attachContractsMap, serializeContractsMap, + transferOwnershipTransactions, } from '../contracts/contracts.js'; import { HyperlaneAddresses, @@ -17,7 +18,6 @@ import { } from '../contracts/types.js'; import { DeployedCoreAddresses } from '../core/schemas.js'; import { CoreConfig } from '../core/types.js'; -import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { ProxyFactoryFactories, @@ -202,12 +202,13 @@ export class EvmCoreModule extends HyperlaneModule< actualConfig: CoreConfig, expectedConfig: CoreConfig, ): AnnotatedEV5Transaction[] { - return EvmModuleDeployer.createTransferOwnershipTx({ - actualOwner: actualConfig.owner, - expectedOwner: expectedConfig.owner, - deployedAddress: this.args.addresses.mailbox, - chainId: this.domainId, - }); + return transferOwnershipTransactions( + this.domainId, + this.args.addresses.mailbox, + actualConfig, + expectedConfig, + 'Mailbox', + ); } /** diff --git a/typescript/sdk/src/deploy/EvmModuleDeployer.ts b/typescript/sdk/src/deploy/EvmModuleDeployer.ts deleted file mode 100644 index 6b2a09eba..000000000 --- a/typescript/sdk/src/deploy/EvmModuleDeployer.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { ethers } from 'ethers'; -import { Logger } from 'pino'; - -import { - Ownable__factory, - StaticAddressSetFactory, - StaticThresholdAddressSetFactory, - TransparentUpgradeableProxy__factory, -} from '@hyperlane-xyz/core'; -import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; -import { - Address, - addBufferToGasLimit, - eqAddress, - rootLogger, -} from '@hyperlane-xyz/utils'; - -import { HyperlaneContracts, HyperlaneFactories } from '../contracts/types.js'; -import { MultiProvider } from '../providers/MultiProvider.js'; -import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; -import { ChainMap, ChainName } from '../types.js'; - -import { isProxy, proxyConstructorArgs } from './proxy.js'; -import { ContractVerifier } from './verify/ContractVerifier.js'; -import { - ContractVerificationInput, - ExplorerLicenseType, -} from './verify/types.js'; -import { getContractVerificationInput } from './verify/utils.js'; - -export class EvmModuleDeployer { - public verificationInputs: ChainMap = {}; - - constructor( - protected readonly multiProvider: MultiProvider, - protected readonly factories: Factories, - protected readonly logger = rootLogger.child({ - module: 'EvmModuleDeployer', - }), - protected readonly contractVerifier?: ContractVerifier, - ) { - this.contractVerifier ??= new ContractVerifier( - multiProvider, - {}, - coreBuildArtifact, - ExplorerLicenseType.MIT, - ); - } - - // Deploys a contract from a factory - public async deployContractFromFactory({ - chain, - factory, - contractName, - constructorArgs, - initializeArgs, - implementationAddress, - }: { - chain: ChainName; - factory: F; - contractName: string; - constructorArgs: Parameters; - initializeArgs?: Parameters>['initialize']>; - implementationAddress?: Address; - }): Promise> { - this.logger.info( - `Deploying ${contractName} on ${chain} with constructor args (${constructorArgs.join( - ', ', - )})...`, - ); - const contract = await this.multiProvider.handleDeploy( - chain, - factory, - constructorArgs, - ); - - if (initializeArgs) { - this.logger.debug(`Initialize ${contractName} on ${chain}`); - // Estimate gas for the initialize transaction - const estimatedGas = await contract.estimateGas.initialize( - ...initializeArgs, - ); - - // deploy with buffer on gas limit - const overrides = this.multiProvider.getTransactionOverrides(chain); - const initTx = await contract.initialize(...initializeArgs, { - gasLimit: addBufferToGasLimit(estimatedGas), - ...overrides, - }); - - await this.multiProvider.handleTx(chain, initTx); - } - - const verificationInput = getContractVerificationInput({ - name: contractName, - contract, - bytecode: factory.bytecode, - expectedimplementation: implementationAddress, - }); - this.addVerificationArtifacts({ chain, artifacts: [verificationInput] }); - - // try verifying contract - try { - await this.contractVerifier?.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 function is 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} constructorArgs - Arguments for the contract's constructor. - * @param {Parameters>['initialize']>?} initializeArgs - Optional arguments for the contract's initialization function. - * @returns {Promise[K]>} A promise that resolves to the deployed contract instance. - */ - public async deployContractWithName({ - chain, - contractKey, - contractName, - constructorArgs, - initializeArgs, - }: { - chain: ChainName; - contractKey: K; - contractName: string; - constructorArgs: Parameters; - initializeArgs?: Parameters< - Awaited>['initialize'] - >; - }): Promise[K]> { - const contract = await this.deployContractFromFactory({ - chain, - factory: this.factories[contractKey], - contractName, - constructorArgs, - initializeArgs, - }); - return contract; - } - - // Deploys a contract with the same name as the contract key - public async deployContract({ - chain, - contractKey, - constructorArgs, - initializeArgs, - }: { - chain: ChainName; - contractKey: K; - constructorArgs: Parameters; - initializeArgs?: Parameters< - Awaited>['initialize'] - >; - }): Promise[K]> { - return this.deployContractWithName({ - chain, - contractKey, - contractName: contractKey.toString(), - constructorArgs, - initializeArgs, - }); - } - - // Deploys the Implementation and Proxy for a given contract - public async deployProxiedContract({ - chain, - contractKey, - contractName, - proxyAdmin, - constructorArgs, - initializeArgs, - }: { - chain: ChainName; - contractKey: K; - contractName: string; - proxyAdmin: string; - constructorArgs: Parameters; - initializeArgs?: Parameters[K]['initialize']>; - }): Promise[K]> { - // 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 - return this.deployProxy({ - chain, - implementation, - proxyAdmin, - initializeArgs, - }); - } - - // Deploys a proxy for a given implementation contract - protected async deployProxy({ - chain, - implementation, - proxyAdmin, - initializeArgs, - }: { - chain: ChainName; - implementation: C; - proxyAdmin: string; - initializeArgs?: Parameters; - }): Promise { - 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, - ); - const proxy = await this.deployContractFromFactory({ - chain, - factory: new TransparentUpgradeableProxy__factory(), - contractName: 'TransparentUpgradeableProxy', - constructorArgs, - implementationAddress: implementation.address, - }); - - return implementation.attach(proxy.address) as C; - } - - // Adds verification artifacts to the verificationInputs map - protected addVerificationArtifacts({ - chain, - artifacts, - }: { - chain: ChainName; - artifacts: ContractVerificationInput[]; - }): void { - this.verificationInputs[chain] = this.verificationInputs[chain] || []; - artifacts.forEach((artifact) => { - this.verificationInputs[chain].push(artifact); - }); - } - - // Static deploy function used by Hook and ISM modules. - public static async deployStaticAddressSet({ - chain, - factory, - values, - logger, - threshold = values.length, - multiProvider, - }: { - chain: ChainName; - factory: StaticThresholdAddressSetFactory | StaticAddressSetFactory; - values: Address[]; - logger: Logger; - threshold?: number; - multiProvider: MultiProvider; - }): Promise
{ - const sortedValues = [...values].sort(); - - const address = await factory['getAddress(address[],uint8)']( - sortedValues, - threshold, - ); - const code = await multiProvider.getProvider(chain).getCode(address); - if (code === '0x') { - logger.debug( - `Deploying new ${threshold} of ${sortedValues.length} address set to ${chain}`, - ); - const overrides = multiProvider.getTransactionOverrides(chain); - - // estimate gas - const estimatedGas = await factory.estimateGas['deploy(address[],uint8)']( - sortedValues, - threshold, - overrides, - ); - - // add gas buffer - const hash = await factory['deploy(address[],uint8)']( - sortedValues, - threshold, - { - gasLimit: addBufferToGasLimit(estimatedGas), - ...overrides, - }, - ); - - await multiProvider.handleTx(chain, hash); - } else { - logger.debug( - `Recovered ${threshold} of ${sortedValues.length} address set on ${chain}: ${address}`, - ); - } - - // TODO: figure out how to get the constructor arguments for manual deploy TXs - // const verificationInput = buildVerificationInput( - // NAME, - // ADDRESS, - // CONSTRUCTOR_ARGS, - // ); - // await this.deployer.verifyContract( - // this.chainName, - // verificationInput, - // logger, - // ); - - return address; - } - - /** - * Transfers ownership of a contract to a new owner. - * - * @param actualOwner - The current owner of the contract. - * @param expectedOwner - The expected new owner of the contract. - * @param deployedAddress - The address of the deployed contract. - * @param chainId - The chain ID of the network the contract is deployed on. - * @returns An array of annotated EV5 transactions that need to be executed to update the owner. - */ - public static createTransferOwnershipTx(params: { - actualOwner: Address; - expectedOwner: Address; - deployedAddress: Address; - chainId: number; - }): AnnotatedEV5Transaction[] { - const { actualOwner, expectedOwner, deployedAddress, chainId } = params; - const updateTransactions: AnnotatedEV5Transaction[] = []; - if (eqAddress(actualOwner, expectedOwner)) { - return []; - } - - updateTransactions.push({ - annotation: `Transferring ownership of ${deployedAddress} from current owner ${actualOwner} to new owner ${expectedOwner}`, - chainId, - to: deployedAddress, - data: Ownable__factory.createInterface().encodeFunctionData( - 'transferOwnership(address)', - [expectedOwner], - ), - }); - - return updateTransactions; - } -} diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index 8203c7919..c6cd2048c 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -74,6 +74,8 @@ export abstract class HyperlaneDeployer< public cachedAddresses: HyperlaneAddressesMap = {}; public deployedContracts: HyperlaneContractsMap = {}; + protected cachingEnabled = true; + protected logger: Logger; chainTimeoutMs: number; @@ -86,7 +88,6 @@ export abstract class HyperlaneDeployer< ) { this.logger = options?.logger ?? rootLogger.child({ module: 'deployer' }); this.chainTimeoutMs = options?.chainTimeoutMs ?? 15 * 60 * 1000; // 15 minute timeout per chain - this.options.ismFactory?.setDeployer(this); if (Object.keys(icaAddresses).length > 0) { this.options.icaApp = InterchainAccount.fromAddressesMap( icaAddresses, @@ -374,7 +375,7 @@ export abstract class HyperlaneDeployer< shouldRecover = true, implementationAddress?: Address, ): Promise> { - if (shouldRecover) { + if (this.cachingEnabled && shouldRecover) { const cachedContract = this.readCache(chain, factory, contractName); if (cachedContract) { if (this.recoverVerificationInputs) { diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index fb8e12842..a1b6d1412 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -40,16 +40,16 @@ import { HyperlaneModuleParams, } from '../core/AbstractHyperlaneModule.js'; import { CoreAddresses } from '../core/contracts.js'; -import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js'; +import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; -import { IgpFactories, igpFactories } from '../gas/contracts.js'; import { IgpConfig } from '../gas/types.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; +import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { ArbL2ToL1IsmConfig, IsmType, OpStackIsmConfig } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; -import { ChainNameOrId } from '../types.js'; +import { ChainName, ChainNameOrId } from '../types.js'; import { normalizeConfig } from '../utils/ism.js'; import { EvmHookReader } from './EvmHookReader.js'; @@ -75,6 +75,14 @@ type HookModuleAddresses = { proxyAdmin: Address; }; +class HookDeployer extends HyperlaneDeployer<{}, HookFactories> { + protected cachingEnabled = false; + + deployContracts(_chain: ChainName, _config: {}): Promise { + throw new Error('Method not implemented.'); + } +} + export class EvmHookModule extends HyperlaneModule< ProtocolType.Ethereum, HookConfig, @@ -82,7 +90,9 @@ export class EvmHookModule extends HyperlaneModule< > { protected readonly logger = rootLogger.child({ module: 'EvmHookModule' }); protected readonly reader: EvmHookReader; - protected readonly deployer: EvmModuleDeployer; + // "ISM" Factory has aggregation hook factories too + protected readonly hookFactory: HyperlaneIsmFactory; + protected readonly deployer: HookDeployer; // Adding these to reduce how often we need to grab from MultiProvider. public readonly chain: string; @@ -105,15 +115,11 @@ export class EvmHookModule extends HyperlaneModule< super(params); this.reader = new EvmHookReader(multiProvider, this.args.chain); - this.deployer = new EvmModuleDeployer( + this.hookFactory = HyperlaneIsmFactory.fromAddressesMap( + { [this.args.chain]: params.addresses }, multiProvider, - { - ...hookFactories, - ...igpFactories, - }, - this.logger, - contractVerifier, ); + this.deployer = new HookDeployer(multiProvider, hookFactories); this.chain = this.multiProvider.getChainName(this.args.chain); this.domainId = this.multiProvider.getDomainId(this.chain); @@ -625,11 +631,9 @@ export class EvmHookModule extends HyperlaneModule< switch (config.type) { case HookType.MERKLE_TREE: - return this.deployer.deployContract({ - chain: this.chain, - contractKey: HookType.MERKLE_TREE, - constructorArgs: [this.args.addresses.mailbox], - }); + return this.deployer.deployContract(this.chain, HookType.MERKLE_TREE, [ + this.args.addresses.mailbox, + ]); case HookType.INTERCHAIN_GAS_PAYMASTER: return this.deployIgpHook({ config }); case HookType.AGGREGATION: @@ -657,16 +661,13 @@ export class EvmHookModule extends HyperlaneModule< config: ProtocolFeeHookConfig; }): Promise { this.logger.debug('Deploying ProtocolFeeHook...'); - return this.deployer.deployContract({ - chain: this.chain, - contractKey: HookType.PROTOCOL_FEE, - constructorArgs: [ - config.maxProtocolFee, - config.protocolFee, - config.beneficiary, - config.owner, - ], - }); + const deployer = new HookDeployer(this.multiProvider, hookFactories); + return deployer.deployContract(this.chain, HookType.PROTOCOL_FEE, [ + config.maxProtocolFee, + config.protocolFee, + config.beneficiary, + config.owner, + ]); } protected async deployPausableHook({ @@ -675,11 +676,12 @@ export class EvmHookModule extends HyperlaneModule< config: PausableHookConfig; }): Promise { this.logger.debug('Deploying PausableHook...'); - const hook = await this.deployer.deployContract({ - chain: this.chain, - contractKey: HookType.PAUSABLE, - constructorArgs: [], - }); + const deployer = new HookDeployer(this.multiProvider, hookFactories); + const hook = await deployer.deployContract( + this.chain, + HookType.PAUSABLE, + [], + ); // transfer ownership await this.multiProvider.handleTx( @@ -715,13 +717,12 @@ export class EvmHookModule extends HyperlaneModule< this.args.addresses.staticAggregationHookFactory, signer, ); - const address = await EvmModuleDeployer.deployStaticAddressSet({ - chain: this.chain, + const address = await this.hookFactory.deployStaticAddressSet( + this.chain, factory, - values: aggregatedHooks, - logger: this.logger, - multiProvider: this.multiProvider, - }); + aggregatedHooks, + this.logger, + ); // return aggregation hook return StaticAggregationHook__factory.connect(address, signer); @@ -773,16 +774,12 @@ export class EvmHookModule extends HyperlaneModule< ); // deploy opstack hook - const hook = await this.deployer.deployContract({ - chain, - contractKey: HookType.OP_STACK, - constructorArgs: [ - mailbox, - this.multiProvider.getDomainId(config.destinationChain), - addressToBytes32(opstackIsm.address), - config.nativeBridge, - ], - }); + const hook = await this.deployer.deployContract(chain, HookType.OP_STACK, [ + mailbox, + this.multiProvider.getDomainId(config.destinationChain), + addressToBytes32(opstackIsm.address), + config.nativeBridge, + ]); // set authorized hook on opstack ism const authorizedHook = await opstackIsm.authorizedHook(); @@ -866,17 +863,17 @@ export class EvmHookModule extends HyperlaneModule< ); // deploy arbL1ToL1 hook - const hook = await this.deployer.deployContract({ + const hook = await this.deployer.deployContract( chain, - contractKey: HookType.ARB_L2_TO_L1, - constructorArgs: [ + HookType.ARB_L2_TO_L1, + [ mailbox, this.multiProvider.getDomainId(config.destinationChain), addressToBytes32(arbL2ToL1IsmAddress), config.arbSys, BigNumber.from(200_000), // 2x estimate of executeTransaction call overhead ], - }); + ); // set authorized hook on arbL2ToL1 ism const authorizedHook = await arbL2ToL1Ism.authorizedHook(); if (authorizedHook === addressToBytes32(hook.address)) { @@ -928,22 +925,18 @@ export class EvmHookModule extends HyperlaneModule< // deploy fallback hook const fallbackHook = await this.deploy({ config: config.fallback }); // deploy routing hook with fallback - routingHook = await this.deployer.deployContract({ - chain: this.chain, - contractKey: HookType.FALLBACK_ROUTING, - constructorArgs: [ - this.args.addresses.mailbox, - deployerAddress, - fallbackHook.address, - ], - }); + routingHook = await this.deployer.deployContract( + this.chain, + HookType.FALLBACK_ROUTING, + [this.args.addresses.mailbox, deployerAddress, fallbackHook.address], + ); } else { // deploy routing hook - routingHook = await this.deployer.deployContract({ - chain: this.chain, - contractKey: HookType.ROUTING, - constructorArgs: [this.args.addresses.mailbox, deployerAddress], - }); + routingHook = await this.deployer.deployContract( + this.chain, + HookType.ROUTING, + [this.args.addresses.mailbox, deployerAddress], + ); } // compute the hooks that need to be set @@ -1002,14 +995,14 @@ export class EvmHookModule extends HyperlaneModule< ); // Deploy the InterchainGasPaymaster - const igp = await this.deployer.deployProxiedContract({ - chain: this.chain, - contractKey: HookType.INTERCHAIN_GAS_PAYMASTER, - contractName: HookType.INTERCHAIN_GAS_PAYMASTER, - proxyAdmin: this.args.addresses.proxyAdmin, - constructorArgs: [], - initializeArgs: [deployerAddress, config.beneficiary], - }); + const igp = await this.deployer.deployProxiedContract( + this.chain, + HookType.INTERCHAIN_GAS_PAYMASTER, + HookType.INTERCHAIN_GAS_PAYMASTER, + this.args.addresses.proxyAdmin, + [], + [deployerAddress, config.beneficiary], + ); // Obtain the transactions to set the gas params for each remote const configureTxs = await this.updateIgpRemoteGasParams({ @@ -1038,11 +1031,12 @@ export class EvmHookModule extends HyperlaneModule< config: IgpConfig; }): Promise { // Deploy the StorageGasOracle, by default msg.sender is the owner - const gasOracle = await this.deployer.deployContract({ - chain: this.chain, - contractKey: 'storageGasOracle', - constructorArgs: [], - }); + const gasOracle = await this.deployer.deployContractFromFactory( + this.chain, + new StorageGasOracle__factory(), + 'storageGasOracle', + [], + ); // Obtain the transactions to set the gas params for each remote const configureTxs = await this.updateStorageGasOracle({ diff --git a/typescript/sdk/src/ism/EvmIsmModule.ts b/typescript/sdk/src/ism/EvmIsmModule.ts index 691ae1deb..bba38aa57 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.ts @@ -1,63 +1,38 @@ import { ethers } from 'ethers'; import { Logger } from 'pino'; -import { - ArbL2ToL1Ism__factory, - DefaultFallbackRoutingIsm__factory, - DomainRoutingIsm, - DomainRoutingIsmFactory__factory, - DomainRoutingIsm__factory, - IAggregationIsm, - IAggregationIsm__factory, - IInterchainSecurityModule__factory, - IMultisigIsm, - IMultisigIsm__factory, - IRoutingIsm, - OPStackIsm__factory, - Ownable__factory, - PausableIsm__factory, - TestIsm__factory, - TrustedRelayerIsm__factory, -} from '@hyperlane-xyz/core'; +import { DomainRoutingIsm__factory } from '@hyperlane-xyz/core'; import { Address, Domain, ProtocolType, - addBufferToGasLimit, assert, deepEquals, - eqAddress, - objFilter, + intersection, rootLogger, } from '@hyperlane-xyz/utils'; -import { attachAndConnectContracts } from '../contracts/contracts.js'; -import { HyperlaneAddresses, HyperlaneContracts } from '../contracts/types.js'; +import { transferOwnershipTransactions } from '../contracts/contracts.js'; +import { HyperlaneAddresses } from '../contracts/types.js'; import { HyperlaneModule, HyperlaneModuleParams, } from '../core/AbstractHyperlaneModule.js'; -import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js'; -import { - ProxyFactoryFactories, - proxyFactoryFactories, -} from '../deploy/contracts.js'; +import { ProxyFactoryFactories } from '../deploy/contracts.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { ChainName, ChainNameOrId } from '../types.js'; import { normalizeConfig } from '../utils/ism.js'; -import { findMatchingLogEvents } from '../utils/logUtils.js'; import { EvmIsmReader } from './EvmIsmReader.js'; +import { HyperlaneIsmFactory } from './HyperlaneIsmFactory.js'; import { IsmConfigSchema } from './schemas.js'; import { - AggregationIsmConfig, DeployedIsm, IsmConfig, IsmType, MUTABLE_ISM_TYPE, - MultisigIsmConfig, RoutingIsmConfig, } from './types.js'; import { calculateDomainRoutingDelta } from './utils.js'; @@ -74,8 +49,8 @@ export class EvmIsmModule extends HyperlaneModule< > { protected readonly logger = rootLogger.child({ module: 'EvmIsmModule' }); protected readonly reader: EvmIsmReader; - protected readonly deployer: EvmModuleDeployer; - protected readonly factories: HyperlaneContracts; + protected readonly ismFactory: HyperlaneIsmFactory; + protected readonly mailbox: Address; // Adding these to reduce how often we need to grab from MultiProvider. public readonly chain: ChainName; @@ -95,33 +70,14 @@ export class EvmIsmModule extends HyperlaneModule< super(params); this.reader = new EvmIsmReader(multiProvider, params.chain); - this.deployer = new EvmModuleDeployer( - this.multiProvider, - {}, - this.logger, - contractVerifier, - ); - this.factories = attachAndConnectContracts( - { - staticMerkleRootMultisigIsmFactory: - params.addresses.staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory: - params.addresses.staticMessageIdMultisigIsmFactory, - staticAggregationIsmFactory: - params.addresses.staticAggregationIsmFactory, - staticAggregationHookFactory: - params.addresses.staticAggregationHookFactory, - domainRoutingIsmFactory: params.addresses.domainRoutingIsmFactory, - staticMerkleRootWeightedMultisigIsmFactory: - params.addresses.staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory: - params.addresses.staticMessageIdWeightedMultisigIsmFactory, - }, - proxyFactoryFactories, - multiProvider.getSigner(params.chain), + this.ismFactory = HyperlaneIsmFactory.fromAddressesMap( + { [params.chain]: params.addresses }, + multiProvider, ); + this.mailbox = params.addresses.mailbox; + this.chain = this.multiProvider.getChainName(this.args.chain); this.domainId = this.multiProvider.getDomainId(this.chain); } @@ -211,24 +167,14 @@ export class EvmIsmModule extends HyperlaneModule< } // Lastly, check if the resolved owner is different from the current owner - const provider = this.multiProvider.getProvider(this.chain); - const owner = await Ownable__factory.connect( - this.args.addresses.deployedIsm, - provider, - ).owner(); - - // Return an ownership transfer transaction if required - if (!eqAddress(targetConfig.owner, owner)) { - updateTxs.push({ - annotation: 'Transferring ownership of ownable ISM...', - chainId: this.domainId, - to: this.args.addresses.deployedIsm, - data: Ownable__factory.createInterface().encodeFunctionData( - 'transferOwnership(address)', - [targetConfig.owner], - ), - }); - } + updateTxs.push( + ...transferOwnershipTransactions( + this.domainId, + this.args.addresses.deployedIsm, + currentConfig, + targetConfig, + ), + ); return updateTxs; } @@ -278,30 +224,24 @@ export class EvmIsmModule extends HyperlaneModule< target: RoutingIsmConfig; logger: Logger; }): Promise { - const routingIsmInterface = DomainRoutingIsm__factory.createInterface(); - const updateTxs = []; - - // filter out domains which are not part of the multiprovider - current = { - ...current, - domains: this.filterRoutingIsmDomains({ - config: current, - }).availableDomains, - }; - target = { - ...target, - domains: this.filterRoutingIsmDomains({ - config: target, - }).availableDomains, - }; + const contract = DomainRoutingIsm__factory.connect( + this.args.addresses.deployedIsm, + this.multiProvider.getProvider(this.chain), + ); + + const updateTxs: AnnotatedEV5Transaction[] = []; + + const knownChains = new Set(this.multiProvider.getKnownChainNames()); const { domainsToEnroll, domainsToUnenroll } = calculateDomainRoutingDelta( current, target, ); + const knownEnrolls = intersection(knownChains, new Set(domainsToEnroll)); + // Enroll domains - for (const origin of domainsToEnroll) { + for (const origin of knownEnrolls) { logger.debug( `Reconfiguring preexisting routing ISM for origin ${origin}...`, ); @@ -310,27 +250,27 @@ export class EvmIsmModule extends HyperlaneModule< }); const domainId = this.multiProvider.getDomainId(origin); + const tx = await contract.populateTransaction.set(domainId, ism.address); updateTxs.push({ annotation: `Setting new ISM for origin ${origin}...`, + ...tx, chainId: this.domainId, - to: this.args.addresses.deployedIsm, - data: routingIsmInterface.encodeFunctionData('set(uint32,address)', [ - domainId, - ism.address, - ]), }); } + const knownUnenrolls = intersection( + knownChains, + new Set(domainsToUnenroll), + ); + // Unenroll domains - for (const origin of domainsToUnenroll) { + for (const origin of knownUnenrolls) { const domainId = this.multiProvider.getDomainId(origin); + const tx = await contract.populateTransaction.remove(domainId); updateTxs.push({ annotation: `Unenrolling originDomain ${domainId} from preexisting routing ISM at ${this.args.addresses.deployedIsm}...`, + ...tx, chainId: this.domainId, - to: this.args.addresses.deployedIsm, - data: routingIsmInterface.encodeFunctionData('remove(uint32)', [ - domainId, - ]), }); } @@ -344,275 +284,10 @@ export class EvmIsmModule extends HyperlaneModule< }): Promise { config = IsmConfigSchema.parse(config); - // If it's an address ISM, just return a base ISM - if (typeof config === 'string') { - // TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3773 - // we can remove the ts-ignore once we have a proper type for address ISMs - // @ts-ignore - return IInterchainSecurityModule__factory.connect( - config, - this.multiProvider.getSignerOrProvider(this.args.chain), - ); - } - - const ismType = config.type; - const logger = rootLogger.child({ chainName: this.chain, ismType }); - - logger.debug(`Deploying ${ismType} to ${this.args.chain}`); - - switch (ismType) { - case IsmType.MESSAGE_ID_MULTISIG: - case IsmType.MERKLE_ROOT_MULTISIG: - return this.deployMultisigIsm({ - config, - logger, - }); - - case IsmType.ROUTING: - case IsmType.FALLBACK_ROUTING: - return this.deployRoutingIsm({ - config, - logger, - }); - - case IsmType.AGGREGATION: - return this.deployAggregationIsm({ - config, - logger, - }); - - case IsmType.OP_STACK: - return this.deployer.deployContractFromFactory({ - chain: this.chain, - factory: new OPStackIsm__factory(), - contractName: IsmType.OP_STACK, - constructorArgs: [config.nativeBridge], - }); - - case IsmType.ARB_L2_TO_L1: - return this.deployer.deployContractFromFactory({ - chain: this.chain, - factory: new ArbL2ToL1Ism__factory(), - contractName: IsmType.ARB_L2_TO_L1, - constructorArgs: [config.bridge], - }); - - case IsmType.PAUSABLE: - return this.deployer.deployContractFromFactory({ - chain: this.chain, - factory: new PausableIsm__factory(), - contractName: IsmType.PAUSABLE, - constructorArgs: [config.owner], - }); - - case IsmType.TRUSTED_RELAYER: - assert( - this.args.addresses.mailbox, - `Mailbox address is required for deploying ${ismType}`, - ); - return this.deployer.deployContractFromFactory({ - chain: this.chain, - factory: new TrustedRelayerIsm__factory(), - contractName: IsmType.TRUSTED_RELAYER, - constructorArgs: [this.args.addresses.mailbox, config.relayer], - }); - - case IsmType.TEST_ISM: - return this.deployer.deployContractFromFactory({ - chain: this.chain, - factory: new TestIsm__factory(), - contractName: IsmType.TEST_ISM, - constructorArgs: [], - }); - - default: - throw new Error(`Unsupported ISM type ${ismType}`); - } - } - - protected async deployMultisigIsm({ - config, - logger, - }: { - config: MultisigIsmConfig; - logger: Logger; - }): Promise { - const signer = this.multiProvider.getSigner(this.chain); - const factoryName = - config.type === IsmType.MERKLE_ROOT_MULTISIG - ? 'staticMerkleRootMultisigIsmFactory' - : 'staticMessageIdMultisigIsmFactory'; - - const address = await EvmModuleDeployer.deployStaticAddressSet({ - chain: this.chain, - factory: this.factories[factoryName], - values: config.validators, - logger, - threshold: config.threshold, - multiProvider: this.multiProvider, - }); - - return IMultisigIsm__factory.connect(address, signer); - } - - protected async deployRoutingIsm({ - config, - logger, - }: { - config: RoutingIsmConfig; - logger: Logger; - }): Promise { - // filter out domains which are not part of the multiprovider - const { availableDomains, availableDomainIds } = - this.filterRoutingIsmDomains({ - config, - }); - config = { - ...config, - domains: availableDomains, - }; - - // deploy the submodules first - const submoduleAddresses: Address[] = []; - for (const origin of Object.keys(config.domains)) { - const { address } = await this.deploy({ - config: config.domains[origin], - }); - submoduleAddresses.push(address); - } - - if (config.type === IsmType.FALLBACK_ROUTING) { - // deploy the fallback routing ISM - logger.debug('Deploying fallback routing ISM ...'); - const ism = await this.multiProvider.handleDeploy( - this.chain, - new DefaultFallbackRoutingIsm__factory(), - [this.args.addresses.mailbox], - ); - - // initialize the fallback routing ISM - logger.debug('Initializing fallback routing ISM ...'); - const tx = await ism['initialize(address,uint32[],address[])']( - config.owner, - availableDomainIds, - submoduleAddresses, - this.multiProvider.getTransactionOverrides(this.args.chain), - ); - - await this.multiProvider.handleTx(this.chain, tx); - // return the fallback routing ISM - return ism; - } - - // then deploy the domain routing ISM - logger.debug('Deploying domain routing ISM ...'); - return this.deployDomainRoutingIsm({ - owner: config.owner, - domainIds: availableDomainIds, - submoduleAddresses, - }); - } - - protected async deployDomainRoutingIsm({ - owner, - domainIds, - submoduleAddresses, - }: { - owner: string; - domainIds: number[]; - submoduleAddresses: string[]; - }): Promise { - const overrides = this.multiProvider.getTransactionOverrides( - this.args.chain, - ); - - const signer = this.multiProvider.getSigner(this.args.chain); - const domainRoutingIsmFactory = DomainRoutingIsmFactory__factory.connect( - this.args.addresses.domainRoutingIsmFactory, - signer, - ); - - // estimate gas - const estimatedGas = await domainRoutingIsmFactory.estimateGas.deploy( - owner, - domainIds, - submoduleAddresses, - overrides, - ); - - // deploying new domain routing ISM, add gas buffer - const tx = await domainRoutingIsmFactory.deploy( - owner, - domainIds, - submoduleAddresses, - { - gasLimit: addBufferToGasLimit(estimatedGas), - ...overrides, - }, - ); - - const receipt = await this.multiProvider.handleTx(this.args.chain, tx); - const dispatchLogs = findMatchingLogEvents( - receipt.logs, - domainRoutingIsmFactory.interface, - 'ModuleDeployed', - ); - - if (dispatchLogs.length === 0) { - throw new Error('No ModuleDeployed event found'); - } - - const moduleAddress = dispatchLogs[0].args['module']; - return DomainRoutingIsm__factory.connect(moduleAddress, signer); - } - - protected async deployAggregationIsm({ - config, - logger, - }: { - config: AggregationIsmConfig; - logger: Logger; - }): Promise { - const addresses: Address[] = []; - // Needs to be deployed sequentially because Ethers will throw `Error: replacement fee too low` - for (const module of config.modules) { - const submodule = await this.deploy({ config: module }); - addresses.push(submodule.address); - } - - const factoryName = 'staticAggregationIsmFactory'; - const address = await EvmModuleDeployer.deployStaticAddressSet({ - chain: this.chain, - factory: this.factories[factoryName], - values: addresses, - logger: logger, - threshold: config.threshold, - multiProvider: this.multiProvider, + return this.ismFactory.deploy({ + destination: this.chain, + config, + mailbox: this.mailbox, }); - - const signer = this.multiProvider.getSigner(this.args.chain); - return IAggregationIsm__factory.connect(address, signer); - } - - // filtering out domains which are not part of the multiprovider - private filterRoutingIsmDomains({ config }: { config: RoutingIsmConfig }) { - const availableDomainIds: number[] = []; - const availableDomains = objFilter( - config.domains, - (domain, _): _ is IsmConfig => { - const domainId = this.multiProvider.tryGetDomainId(domain); - if (domainId === null) { - this.logger.warn( - `Domain ${domain} doesn't have chain metadata provided, skipping ...`, - ); - return false; - } - - availableDomainIds.push(domainId); - return true; - }, - ); - - return { availableDomains, availableDomainIds }; } } diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts index 5e7b004a0..894eaa278 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts @@ -168,7 +168,6 @@ describe('HyperlaneIsmFactory', async () => { ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); ismFactory = new HyperlaneIsmFactory(contractsMap, multiProvider); - ismFactory.setDeployer(new TestCoreDeployer(multiProvider, ismFactory)); exampleRoutingConfig = { type: IsmType.ROUTING, diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index c2f42b8db..645fee8aa 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -2,6 +2,7 @@ import { ethers } from 'ethers'; import { Logger } from 'pino'; import { + ArbL2ToL1Ism__factory, DefaultFallbackRoutingIsm, DefaultFallbackRoutingIsm__factory, DomainRoutingIsm, @@ -33,7 +34,10 @@ import { import { HyperlaneApp } from '../app/HyperlaneApp.js'; import { appFromAddressesMapHelper } from '../contracts/contracts.js'; -import { HyperlaneAddressesMap } from '../contracts/types.js'; +import { + HyperlaneAddressesMap, + HyperlaneContractsMap, +} from '../contracts/types.js'; import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; import { ProxyFactoryFactories, @@ -55,15 +59,39 @@ import { } from './types.js'; import { routingModuleDelta } from './utils.js'; +const ismFactories = { + [IsmType.PAUSABLE]: new PausableIsm__factory(), + [IsmType.TRUSTED_RELAYER]: new TrustedRelayerIsm__factory(), + [IsmType.TEST_ISM]: new TestIsm__factory(), + [IsmType.OP_STACK]: new OPStackIsm__factory(), + [IsmType.ARB_L2_TO_L1]: new ArbL2ToL1Ism__factory(), +}; + +class IsmDeployer extends HyperlaneDeployer<{}, typeof ismFactories> { + protected readonly cachingEnabled = false; + + deployContracts(_chain: ChainName, _config: any): Promise { + throw new Error('Method not implemented.'); + } +} + export class HyperlaneIsmFactory extends HyperlaneApp { // The shape of this object is `ChainMap
`, // although `any` is use here because that type breaks a lot of signatures. // TODO: fix this in the next refactoring public deployedIsms: ChainMap = {}; + protected readonly deployer: IsmDeployer; - protected deployer?: HyperlaneDeployer; - setDeployer(deployer: HyperlaneDeployer): void { - this.deployer = deployer; + constructor( + contractsMap: HyperlaneContractsMap, + public readonly multiProvider: MultiProvider, + ) { + super( + contractsMap, + multiProvider, + rootLogger.child({ module: 'ismFactoryApp' }), + ); + this.deployer = new IsmDeployer(multiProvider, ismFactories); } static fromAddressesMap( @@ -75,11 +103,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { proxyFactoryFactories, multiProvider, ); - return new HyperlaneIsmFactory( - helper.contractsMap, - multiProvider, - rootLogger.child({ module: 'ismFactoryApp' }), - ); + return new HyperlaneIsmFactory(helper.contractsMap, multiProvider); } async deploy(params: { @@ -142,56 +166,39 @@ export class HyperlaneIsmFactory extends HyperlaneApp { }); break; case IsmType.OP_STACK: - assert( - this.deployer, - `HyperlaneDeployer must be set to deploy ${ismType}`, - ); - contract = await this.deployer.deployContractFromFactory( - destination, - new OPStackIsm__factory(), - IsmType.OP_STACK, - [config.nativeBridge], - ); + contract = await this.deployer.deployContract(destination, ismType, [ + config.nativeBridge, + ]); break; case IsmType.PAUSABLE: - assert( - this.deployer, - `HyperlaneDeployer must be set to deploy ${ismType}`, - ); - contract = await this.deployer.deployContractFromFactory( + contract = await this.deployer.deployContract( destination, - new PausableIsm__factory(), IsmType.PAUSABLE, [config.owner], ); - await this.deployer.transferOwnershipOfContracts(destination, config, { - [IsmType.PAUSABLE]: contract, - }); break; case IsmType.TRUSTED_RELAYER: - assert( - this.deployer, - `HyperlaneDeployer must be set to deploy ${ismType}`, - ); assert(mailbox, `Mailbox address is required for deploying ${ismType}`); - contract = await this.deployer.deployContractFromFactory( + contract = await this.deployer.deployContract( destination, - new TrustedRelayerIsm__factory(), IsmType.TRUSTED_RELAYER, [mailbox, config.relayer], ); break; case IsmType.TEST_ISM: - if (!this.deployer) { - throw new Error(`HyperlaneDeployer must be set to deploy ${ismType}`); - } - contract = await this.deployer.deployContractFromFactory( + contract = await this.deployer.deployContract( destination, - new TestIsm__factory(), IsmType.TEST_ISM, [], ); break; + case IsmType.ARB_L2_TO_L1: + contract = await this.deployer.deployContract( + destination, + IsmType.ARB_L2_TO_L1, + [config.bridge], + ); + break; default: throw new Error(`Unsupported ISM type ${ismType}`); } diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.ts b/typescript/sdk/src/token/EvmERC20WarpModule.ts index 19ea85489..6dec4b093 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.ts @@ -15,11 +15,11 @@ import { rootLogger, } from '@hyperlane-xyz/utils'; +import { transferOwnershipTransactions } from '../contracts/contracts.js'; import { HyperlaneModule, HyperlaneModuleParams, } from '../core/AbstractHyperlaneModule.js'; -import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; @@ -215,12 +215,13 @@ export class EvmERC20WarpModule extends HyperlaneModule< actualConfig: TokenRouterConfig, expectedConfig: TokenRouterConfig, ): AnnotatedEV5Transaction[] { - return EvmModuleDeployer.createTransferOwnershipTx({ - actualOwner: actualConfig.owner, - expectedOwner: expectedConfig.owner, - deployedAddress: this.args.addresses.deployedTokenRoute, - chainId: this.domainId, - }); + return transferOwnershipTransactions( + this.multiProvider.getDomainId(this.args.chain), + this.args.addresses.deployedTokenRoute, + actualConfig, + expectedConfig, + `${expectedConfig.type} Warp Route`, + ); } /** diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index 0c82c6782..26921fa1c 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -103,10 +103,12 @@ export { parseLegacyMultisigIsmMetadata, } from './multisig.js'; export { + ObjectDiff, ValueOf, arrayToObject, deepCopy, deepEquals, + diffObjMerge, invertKeysAndValues, isObjEmpty, isObject, @@ -120,11 +122,14 @@ export { pick, promiseObjAll, stringifyObject, - diffObjMerge, - ObjectDiff, } from './objects.js'; export { Result, failure, success } from './result.js'; -export { difference, setEquality, symmetricDifference } from './sets.js'; +export { + difference, + intersection, + setEquality, + symmetricDifference, +} from './sets.js'; export { errorToString, fromHexString, diff --git a/typescript/utils/src/sets.ts b/typescript/utils/src/sets.ts index 18149ae37..587c00a62 100644 --- a/typescript/utils/src/sets.ts +++ b/typescript/utils/src/sets.ts @@ -22,3 +22,13 @@ export function symmetricDifference(a: Set, b: Set) { export function setEquality(a: Set, b: Set) { return symmetricDifference(a, b).size === 0; } + +export function intersection(a: Set, b: Set) { + const _intersection = new Set(); + a.forEach((elem) => { + if (b.has(elem)) { + _intersection.add(elem); + } + }); + return _intersection; +}