@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
325 lines • 17.7 kB
JavaScript
import { zeroAddress } from 'viem';
import { GasRouter__factory, MailboxClient__factory, TokenRouter__factory, } from '@hyperlane-xyz/core';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import { addressToBytes32, assert, deepEquals, difference, eqAddress, isObjEmpty, objMap, rootLogger, } from '@hyperlane-xyz/utils';
import { transferOwnershipTransactions } from '../contracts/contracts.js';
import { HyperlaneModule, } from '../core/AbstractHyperlaneModule.js';
import { proxyAdminUpdateTxs } from '../deploy/proxy.js';
import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
import { ExplorerLicenseType } from '../deploy/verify/types.js';
import { EvmHookModule } from '../hook/EvmHookModule.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { extractIsmAndHookFactoryAddresses } from '../utils/ism.js';
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { HypERC20Deployer } from './deploy.js';
import { HypTokenRouterConfigSchema, derivedHookAddress, derivedIsmAddress, } from './types.js';
export class EvmERC20WarpModule extends HyperlaneModule {
multiProvider;
ccipContractCache;
contractVerifier;
logger = rootLogger.child({
module: 'EvmERC20WarpModule',
});
reader;
chainName;
chainId;
domainId;
constructor(multiProvider, args, ccipContractCache, contractVerifier) {
super(args);
this.multiProvider = multiProvider;
this.ccipContractCache = ccipContractCache;
this.contractVerifier = contractVerifier;
this.reader = new EvmERC20WarpRouteReader(multiProvider, args.chain);
this.chainName = this.multiProvider.getChainName(args.chain);
this.chainId = multiProvider.getEvmChainId(args.chain);
this.domainId = multiProvider.getDomainId(args.chain);
this.chainId = multiProvider.getEvmChainId(args.chain);
this.contractVerifier ??= new ContractVerifier(multiProvider, {}, coreBuildArtifact, ExplorerLicenseType.MIT);
}
/**
* Retrieves the token router configuration for the specified address.
*
* @param address - The address to derive the token router configuration from.
* @returns A promise that resolves to the token router configuration.
*/
async read() {
return this.reader.deriveWarpRouteConfig(this.args.addresses.deployedTokenRoute);
}
/**
* Updates the Warp Route contract with the provided configuration.
*
* @param expectedConfig - The configuration for the token router to be updated.
* @returns An array of Ethereum transactions that were executed to update the contract, or an error if the update failed.
*/
async update(expectedConfig) {
HypTokenRouterConfigSchema.parse(expectedConfig);
const actualConfig = await this.read();
const transactions = [];
/**
* @remark
* The order of operations matter
* 1. createOwnershipUpdateTxs() must always be LAST because no updates possible after ownership transferred
* 2. createRemoteRoutersUpdateTxs() must always be BEFORE createSetDestinationGasUpdateTxs() because gas enumeration depends on domains
*/
transactions.push(...(await this.createIsmUpdateTxs(actualConfig, expectedConfig)), ...(await this.createHookUpdateTxs(actualConfig, expectedConfig)), ...this.createEnrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createUnenrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig), ...this.createOwnershipUpdateTxs(actualConfig, expectedConfig), ...proxyAdminUpdateTxs(this.chainId, this.args.addresses.deployedTokenRoute, actualConfig, expectedConfig));
return transactions;
}
/**
* Create a transaction to update the remote routers for the Warp Route contract.
*
* @param actualConfig - The on-chain router configuration, including the remoteRouters array.
* @param expectedConfig - The expected token router configuration.
* @returns A array with a single Ethereum transaction that need to be executed to enroll the routers
*/
createEnrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig) {
const updateTransactions = [];
if (!expectedConfig.remoteRouters) {
return [];
}
assert(actualConfig.remoteRouters, 'actualRemoteRouters is undefined');
assert(expectedConfig.remoteRouters, 'actualRemoteRouters is undefined');
const { remoteRouters: actualRemoteRouters } = actualConfig;
const { remoteRouters: expectedRemoteRouters } = expectedConfig;
const routesToEnroll = Object.entries(expectedRemoteRouters)
.filter(([domain, expectedRouter]) => {
const actualRouter = actualRemoteRouters[domain];
// Enroll if router doesn't exist for domain or has different address
return (!actualRouter ||
!eqAddress(actualRouter.address, expectedRouter.address));
})
.map(([domain]) => domain);
if (routesToEnroll.length === 0) {
return updateTransactions;
}
const contractToUpdate = TokenRouter__factory.connect(this.args.addresses.deployedTokenRoute, this.multiProvider.getProvider(this.domainId));
updateTransactions.push({
chainId: this.chainId,
annotation: `Enrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData('enrollRemoteRouters', [
routesToEnroll.map((k) => Number(k)),
routesToEnroll.map((a) => addressToBytes32(expectedRemoteRouters[a].address)),
]),
});
return updateTransactions;
}
createUnenrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig) {
const updateTransactions = [];
if (!expectedConfig.remoteRouters) {
return [];
}
assert(actualConfig.remoteRouters, 'actualRemoteRouters is undefined');
assert(expectedConfig.remoteRouters, 'actualRemoteRouters is undefined');
const { remoteRouters: actualRemoteRouters } = actualConfig;
const { remoteRouters: expectedRemoteRouters } = expectedConfig;
const routesToUnenroll = Array.from(difference(new Set(Object.keys(actualRemoteRouters)), new Set(Object.keys(expectedRemoteRouters))));
if (routesToUnenroll.length === 0) {
return updateTransactions;
}
const contractToUpdate = TokenRouter__factory.connect(this.args.addresses.deployedTokenRoute, this.multiProvider.getProvider(this.domainId));
updateTransactions.push({
annotation: `Unenrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
chainId: this.chainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData('unenrollRemoteRouters(uint32[])', [routesToUnenroll.map((k) => Number(k))]),
});
return updateTransactions;
}
/**
* Create a transaction to update the remote routers for the Warp Route contract.
*
* @param actualConfig - The on-chain router configuration, including the remoteRouters array.
* @param expectedConfig - The expected token router configuration.
* @returns A array with a single Ethereum transaction that need to be executed to enroll the routers
*/
createSetDestinationGasUpdateTxs(actualConfig, expectedConfig) {
const updateTransactions = [];
if (!expectedConfig.destinationGas) {
return [];
}
assert(actualConfig.destinationGas, 'actualDestinationGas is undefined');
assert(expectedConfig.destinationGas, 'actualDestinationGas is undefined');
const { destinationGas: actualDestinationGas } = actualConfig;
const { destinationGas: expectedDestinationGas } = expectedConfig;
if (!deepEquals(actualDestinationGas, expectedDestinationGas)) {
const contractToUpdate = GasRouter__factory.connect(this.args.addresses.deployedTokenRoute, this.multiProvider.getProvider(this.domainId));
// Convert { 1: 2, 2: 3, ... } to [{ 1: 2 }, { 2: 3 }]
const gasRouterConfigs = [];
objMap(expectedDestinationGas, (domain, gas) => {
gasRouterConfigs.push({
domain,
gas,
});
});
updateTransactions.push({
chainId: this.chainId,
annotation: `Setting destination gas for ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData('setDestinationGas((uint32,uint256)[])', [gasRouterConfigs]),
});
}
return updateTransactions;
}
/**
* Create transactions to update an existing ISM config, or deploy a new ISM and return a tx to setInterchainSecurityModule
*
* @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 Ethereum transaction that need to be executed to update the ISM configuration.
*/
async createIsmUpdateTxs(actualConfig, expectedConfig) {
const updateTransactions = [];
if (!expectedConfig.interchainSecurityModule ||
expectedConfig.interchainSecurityModule === zeroAddress) {
return [];
}
const actualDeployedIsm = derivedIsmAddress(actualConfig);
// Try to update (may also deploy) Ism with the expected config
const { deployedIsm: expectedDeployedIsm, updateTransactions: ismUpdateTransactions, } = await this.deployOrUpdateIsm(actualConfig, expectedConfig);
// If an ISM is updated in-place, push the update txs
updateTransactions.push(...ismUpdateTransactions);
// If a new ISM is deployed, push the setInterchainSecurityModule tx
if (actualDeployedIsm !== expectedDeployedIsm) {
const contractToUpdate = MailboxClient__factory.connect(this.args.addresses.deployedTokenRoute, this.multiProvider.getProvider(this.domainId));
updateTransactions.push({
chainId: this.chainId,
annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData('setInterchainSecurityModule', [expectedDeployedIsm]),
});
}
return updateTransactions;
}
async createHookUpdateTxs(actualConfig, expectedConfig) {
const updateTransactions = [];
if (!expectedConfig.hook || expectedConfig.hook === zeroAddress) {
return [];
}
const actualDeployedHook = derivedHookAddress(actualConfig);
// Try to deploy or update Hook with the expected config
const { deployedHook: expectedDeployedHook, updateTransactions: hookUpdateTransactions, } = await this.deployOrUpdateHook(actualConfig, expectedConfig);
// If a Hook is updated in-place, push the update txs
updateTransactions.push(...hookUpdateTransactions);
// If a new Hook is deployed, push the setHook tx
if (!eqAddress(actualDeployedHook, expectedDeployedHook)) {
const contractToUpdate = MailboxClient__factory.connect(this.args.addresses.deployedTokenRoute, this.multiProvider.getProvider(this.domainId));
updateTransactions.push({
chainId: this.chainId,
annotation: `Setting Hook for Warp Route to ${expectedDeployedHook}`,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData('setHook', [
expectedDeployedHook,
]),
});
}
return updateTransactions;
}
/**
* Transfer ownership of an existing Warp route with a given config.
*
* @param actualConfig - The on-chain router configuration.
* @param expectedConfig - The expected token router configuration.
* @returns Ethereum transaction that need to be executed to update the owner.
*/
createOwnershipUpdateTxs(actualConfig, expectedConfig) {
return transferOwnershipTransactions(this.multiProvider.getEvmChainId(this.args.chain), this.args.addresses.deployedTokenRoute, actualConfig, expectedConfig, `${expectedConfig.type} Warp Route`);
}
/**
* Updates or deploys the ISM using the provided configuration.
*
* @returns Object with deployedIsm address, and update Transactions
*/
async deployOrUpdateIsm(actualConfig, expectedConfig) {
assert(expectedConfig.interchainSecurityModule, 'Ism derived incorrectly');
const ismModule = new EvmIsmModule(this.multiProvider, {
chain: this.args.chain,
config: expectedConfig.interchainSecurityModule,
addresses: {
...this.args.addresses,
mailbox: expectedConfig.mailbox,
deployedIsm: derivedIsmAddress(actualConfig),
},
}, this.ccipContractCache, this.contractVerifier);
this.logger.info(`Comparing target ISM config with ${this.args.chain} chain`);
const updateTransactions = await ismModule.update(expectedConfig.interchainSecurityModule);
const { deployedIsm } = ismModule.serialize();
return { deployedIsm, updateTransactions };
}
/**
* Updates or deploys the hook using the provided configuration.
*
* @returns Object with deployedHook address, and update Transactions
*/
async deployOrUpdateHook(actualConfig, expectedConfig) {
assert(expectedConfig.hook, 'No hook config');
if (!actualConfig.hook || actualConfig.hook === zeroAddress) {
return this.deployNewHook(expectedConfig);
}
return this.updateExistingHook(expectedConfig, actualConfig);
}
async deployNewHook(expectedConfig) {
this.logger.info(`No hook deployed for warp route, deploying new hook on ${this.args.chain} chain`);
assert(expectedConfig.hook, 'Hook is undefined');
assert(expectedConfig.proxyAdmin?.address, 'ProxyAdmin address is undefined');
const hookModule = await EvmHookModule.create({
chain: this.args.chain,
config: expectedConfig.hook,
proxyFactoryFactories: extractIsmAndHookFactoryAddresses(this.args.addresses),
coreAddresses: {
mailbox: expectedConfig.mailbox,
proxyAdmin: expectedConfig.proxyAdmin?.address, // Assume that a proxyAdmin is always deployed with a WarpRoute
},
contractVerifier: this.contractVerifier,
multiProvider: this.multiProvider,
});
const { deployedHook } = hookModule.serialize();
return { deployedHook, updateTransactions: [] };
}
async updateExistingHook(expectedConfig, actualConfig) {
assert(actualConfig.proxyAdmin?.address, 'ProxyAdmin address is undefined');
assert(actualConfig.hook, 'Hook is undefined');
const hookModule = new EvmHookModule(this.multiProvider, {
chain: this.args.chain,
config: actualConfig.hook,
addresses: {
...extractIsmAndHookFactoryAddresses(this.args.addresses),
mailbox: actualConfig.mailbox,
proxyAdmin: actualConfig.proxyAdmin?.address,
deployedHook: derivedHookAddress(actualConfig),
},
}, this.ccipContractCache, this.contractVerifier);
this.logger.info(`Comparing target Hook config with ${this.args.chain} chain`);
const updateTransactions = await hookModule.update(expectedConfig.hook);
const { deployedHook } = hookModule.serialize();
return { deployedHook, updateTransactions };
}
/**
* Deploys the Warp Route.
*
* @param chain - The chain to deploy the module on.
* @param config - The configuration for the token router.
* @param multiProvider - The multi-provider instance to use.
* @returns A new instance of the EvmERC20WarpHyperlaneModule.
*/
static async create(params) {
const { chain, config, multiProvider, ccipContractCache, contractVerifier, proxyFactoryFactories, } = params;
const chainName = multiProvider.getChainName(chain);
const deployer = new HypERC20Deployer(multiProvider);
const deployedContracts = await deployer.deployContracts(chainName, config);
const warpModule = new EvmERC20WarpModule(multiProvider, {
addresses: {
...proxyFactoryFactories,
deployedTokenRoute: deployedContracts[config.type].address,
},
chain,
config,
}, ccipContractCache, contractVerifier);
if (config.remoteRouters && !isObjEmpty(config.remoteRouters)) {
const enrollRemoteTxs = await warpModule.update(config); // @TODO Remove when EvmERC20WarpModule.create can be used
const onlyTxIndex = 0;
await multiProvider.sendTransaction(chain, enrollRemoteTxs[onlyTxIndex]);
}
return warpModule;
}
}
//# sourceMappingURL=EvmERC20WarpModule.js.map