@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
478 lines • 27.8 kB
JavaScript
// import { expect } from 'chai';
import { compareVersions } from 'compare-versions';
import { UINT_256_MAX } from 'starknet';
import { zeroAddress } from 'viem';
import { GasRouter__factory, IERC20__factory, MailboxClient__factory, MovableCollateralRouter__factory, ProxyAdmin__factory, TokenRouter__factory, } from '@hyperlane-xyz/core';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import { addressToBytes32, assert, deepEquals, difference, isObjEmpty, normalizeAddressEvm, objMap, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils';
import { transferOwnershipTransactions } from '../contracts/contracts.js';
import { HyperlaneModule, } from '../core/AbstractHyperlaneModule.js';
import { isInitialized, proxyAdmin, proxyAdminUpdateTxs, } from '../deploy/proxy.js';
import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
import { ExplorerLicenseType } from '../deploy/verify/types.js';
import { getEvmHookUpdateTransactions } from '../hook/updates.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { resolveRouterMapConfig } from '../router/types.js';
import { extractIsmAndHookFactoryAddresses } from '../utils/ism.js';
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { hypERC20contracts } from './contracts.js';
import { HypERC20Deployer } from './deploy.js';
import { HypTokenRouterConfigSchema, VERSION_ERROR_MESSAGE, contractVersionMatchesDependency, derivedIsmAddress, isMovableCollateralTokenConfig, } from './types.js';
const getAllowedRebalancingBridgesByDomain = (allowedRebalancingBridgesByDomain) => {
return objMap(allowedRebalancingBridgesByDomain, (_domainId, allowedRebalancingBridges) => {
return new Set(allowedRebalancingBridges.map((bridgeConfig) => normalizeAddressEvm(bridgeConfig.bridge)));
});
};
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.upgradeWarpRouteImplementationTx(actualConfig, expectedConfig)), ...(await this.createIsmUpdateTxs(actualConfig, expectedConfig)), ...(await this.createHookUpdateTxs(actualConfig, expectedConfig)), ...this.createEnrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createUnenrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig), ...this.createAddRebalancersUpdateTxs(actualConfig, expectedConfig), ...this.createRemoveRebalancersUpdateTxs(actualConfig, expectedConfig), ...(await this.createAddAllowedBridgesUpdateTxs(actualConfig, expectedConfig)), ...this.createRemoveBridgesTxs(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)
.map(([domain, rawRouter]) => [
domain,
{ address: addressToBytes32(rawRouter.address) },
])
.filter(([domain, expectedRouter]) => {
const actualRouter = actualRemoteRouters[domain];
// Enroll if router doesn't exist for domain or has different address
return !actualRouter || 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;
}
createAddRebalancersUpdateTxs(actualConfig, expectedConfig) {
actualConfig.type;
if (!isMovableCollateralTokenConfig(expectedConfig) ||
!isMovableCollateralTokenConfig(actualConfig)) {
return [];
}
if (!expectedConfig.allowedRebalancers) {
return [];
}
const formattedExpectedRebalancers = new Set(expectedConfig.allowedRebalancers.map(normalizeAddressEvm));
const formattedActualRebalancers = new Set((actualConfig.allowedRebalancers ?? []).map(normalizeAddressEvm));
const rebalancersToAdd = Array.from(difference(formattedExpectedRebalancers, formattedActualRebalancers));
if (rebalancersToAdd.length === 0) {
return [];
}
return rebalancersToAdd.map((rebalancerToAdd) => ({
chainId: this.chainId,
annotation: `Adding rebalancer role to "${rebalancerToAdd}" on token "${this.args.addresses.deployedTokenRoute}" on chain "${this.chainName}"`,
to: this.args.addresses.deployedTokenRoute,
data: MovableCollateralRouter__factory.createInterface().encodeFunctionData('addRebalancer(address)', [rebalancerToAdd]),
}));
}
createRemoveRebalancersUpdateTxs(actualConfig, expectedConfig) {
actualConfig.type;
if (!isMovableCollateralTokenConfig(expectedConfig) ||
!isMovableCollateralTokenConfig(actualConfig)) {
return [];
}
if (!expectedConfig.allowedRebalancers) {
return [];
}
const formattedExpectedRebalancers = new Set(expectedConfig.allowedRebalancers.map(normalizeAddressEvm));
const formattedActualRebalancers = new Set((actualConfig.allowedRebalancers ?? []).map(normalizeAddressEvm));
const rebalancersToRemove = Array.from(difference(formattedActualRebalancers, formattedExpectedRebalancers));
if (rebalancersToRemove.length === 0) {
return [];
}
return rebalancersToRemove.map((rebalancerToRemove) => ({
chainId: this.chainId,
annotation: `Removing rebalancer role from "${rebalancerToRemove}" on token "${this.args.addresses.deployedTokenRoute}" on chain "${this.chainName}"`,
to: this.args.addresses.deployedTokenRoute,
data: MovableCollateralRouter__factory.createInterface().encodeFunctionData('removeRebalancer(address)', [rebalancerToRemove]),
}));
}
async getAllowedBridgesApprovalTxs(actualConfig, expectedConfig) {
if (!isMovableCollateralTokenConfig(expectedConfig) ||
!isMovableCollateralTokenConfig(actualConfig)) {
return [];
}
if (!expectedConfig.allowedRebalancingBridges) {
return [];
}
const tokensToApproveByAllowedBridge = Object.values(expectedConfig.allowedRebalancingBridges).reduce((acc, allowedBridgesConfigs) => {
allowedBridgesConfigs.forEach((bridgeConfig) => {
acc[bridgeConfig.bridge] ??= [];
acc[bridgeConfig.bridge].push(...(bridgeConfig.approvedTokens ?? []));
});
return acc;
},
// allowed bridge -> tokens to approve
{});
const filteredTokensToApproveByAllowedBridge = await promiseObjAll(objMap(tokensToApproveByAllowedBridge, async (bridge, tokens) => {
const filteredApprovals = [];
for (const token of tokens) {
const instance = IERC20__factory.connect(token, this.multiProvider.getProvider(this.chainId));
const allowance = await instance.allowance(this.args.addresses.deployedTokenRoute, bridge);
if (allowance.toBigInt() !== UINT_256_MAX) {
filteredApprovals.push(token);
}
}
return filteredApprovals;
}));
return Object.entries(filteredTokensToApproveByAllowedBridge).flatMap(([bridge, tokensToApprove]) => tokensToApprove.map((tokenToApprove) => ({
chainId: this.chainId,
annotation: `Approving allowed bridge "${bridge}" to spend token "${tokenToApprove}" on behalf of "${this.args.addresses.deployedTokenRoute}" on chain "${this.chainName}"`,
to: this.args.addresses.deployedTokenRoute,
data: MovableCollateralRouter__factory.createInterface().encodeFunctionData('approveTokenForBridge(address,address)', [tokenToApprove, bridge]),
})));
}
async createAddAllowedBridgesUpdateTxs(actualConfig, expectedConfig) {
if (!isMovableCollateralTokenConfig(expectedConfig) ||
!isMovableCollateralTokenConfig(actualConfig)) {
return [];
}
if (!expectedConfig.allowedRebalancingBridges) {
return [];
}
const actualAllowedBridges = getAllowedRebalancingBridgesByDomain(resolveRouterMapConfig(this.multiProvider, actualConfig.allowedRebalancingBridges ?? {}));
const expectedAllowedBridges = getAllowedRebalancingBridgesByDomain(resolveRouterMapConfig(this.multiProvider, expectedConfig.allowedRebalancingBridges));
const rebalancingBridgesToAddByDomain = objMap(expectedAllowedBridges, (domain, bridges) => {
const actualBridges = actualAllowedBridges[domain] ?? new Set();
return Array.from(difference(bridges, actualBridges));
});
const bridgesToAllow = Object.entries(rebalancingBridgesToAddByDomain).flatMap(([domain, allowedBridgesToAdd]) => {
return allowedBridgesToAdd.map((bridgeToAdd) => {
return {
chainId: this.chainId,
annotation: `Adding allowed bridge "${bridgeToAdd}" on token "${this.args.addresses.deployedTokenRoute}" on chain "${this.chainName}"`,
to: this.args.addresses.deployedTokenRoute,
data: MovableCollateralRouter__factory.createInterface().encodeFunctionData('addBridge(uint32,address)', [domain, bridgeToAdd]),
};
});
});
const approvalTxs = await this.getAllowedBridgesApprovalTxs(actualConfig, expectedConfig);
return [...bridgesToAllow, ...approvalTxs];
}
createRemoveBridgesTxs(actualConfig, expectedConfig) {
if (!isMovableCollateralTokenConfig(expectedConfig) ||
!isMovableCollateralTokenConfig(actualConfig)) {
return [];
}
if (!expectedConfig.allowedRebalancingBridges) {
return [];
}
const actualAllowedBridges = getAllowedRebalancingBridgesByDomain(resolveRouterMapConfig(this.multiProvider, actualConfig.allowedRebalancingBridges ?? {}));
const expectedAllowedBridges = getAllowedRebalancingBridgesByDomain(resolveRouterMapConfig(this.multiProvider, expectedConfig.allowedRebalancingBridges));
const rebalancingBridgesToAddByDomain = objMap(actualAllowedBridges, (domain, bridges) => {
const expectedBridges = expectedAllowedBridges[domain] ?? new Set();
return Array.from(difference(bridges, expectedBridges));
});
return Object.entries(rebalancingBridgesToAddByDomain).flatMap(([domain, allowedBridgesToAdd]) => {
return allowedBridgesToAdd.map((bridgeToAdd) => {
return {
chainId: this.chainId,
annotation: `Removing allowed bridge "${bridgeToAdd}" on token "${this.args.addresses.deployedTokenRoute}" on chain "${this.chainName}"`,
to: this.args.addresses.deployedTokenRoute,
data: MovableCollateralRouter__factory.createInterface().encodeFunctionData('removeBridge(uint32,address)', [domain, bridgeToAdd]),
};
});
});
}
/**
* 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, 'expectedDestinationGas is undefined');
const actualDestinationGas = resolveRouterMapConfig(this.multiProvider, actualConfig.destinationGas);
const expectedDestinationGas = resolveRouterMapConfig(this.multiProvider, expectedConfig.destinationGas);
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) {
if (!expectedConfig.hook) {
return [];
}
const proxyAdminAddress = expectedConfig.proxyAdmin?.address ?? actualConfig.proxyAdmin?.address;
assert(proxyAdminAddress, 'ProxyAdmin address is undefined');
return getEvmHookUpdateTransactions(this.args.addresses.deployedTokenRoute, {
actualConfig: actualConfig.hook,
expectedConfig: expectedConfig.hook,
ccipContractCache: this.ccipContractCache,
contractVerifier: this.contractVerifier,
evmChainName: this.chainName,
hookAndIsmFactories: extractIsmAndHookFactoryAddresses(this.args.addresses),
setHookFunctionCallEncoder: (newHookAddress) => MailboxClient__factory.createInterface().encodeFunctionData('setHook', [newHookAddress]),
logger: this.logger,
mailbox: actualConfig.mailbox,
multiProvider: this.multiProvider,
proxyAdminAddress,
});
}
/**
* 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 };
}
/**
* Creates a transaction to upgrade the Warp Route implementation if the package version is below specified version.
*
* @param actualConfig - The current on-chain configuration
* @param expectedConfig - The expected configuration
* @returns An array of transactions to upgrade the implementation if needed
*/
async upgradeWarpRouteImplementationTx(actualConfig, expectedConfig) {
const updateTransactions = [];
// This should be impossible since we try catch the call to `PACKAGE_VERSION`
// in `EvmERC20WarpRouteReader.fetchPackageVersion`
assert(actualConfig.contractVersion, 'Actual contract version is undefined');
// Only upgrade if the user specifies a version
if (!expectedConfig.contractVersion) {
return [];
}
const comparisonValue = compareVersions(expectedConfig.contractVersion, actualConfig.contractVersion);
// Expected version is lower than actual version, no upgrade is possible
if (comparisonValue === -1) {
throw new Error(`Expected contract version ${expectedConfig.contractVersion} is lower than actual contract version ${actualConfig.contractVersion}`);
}
// Versions are the same, no upgrade needed
if (comparisonValue === 0) {
return [];
}
// You can only upgrade to the contract version (see `PackageVersioned`)
// defined by the @hyperlane-xyz/core package
assert(contractVersionMatchesDependency(expectedConfig.contractVersion), VERSION_ERROR_MESSAGE);
this.logger.info(`Upgrading Warp Route implementation on ${this.args.chain} from ${actualConfig.contractVersion} to ${expectedConfig.contractVersion}`);
const deployer = new HypERC20Deployer(this.multiProvider);
const constructorArgs = await deployer.constructorArgs(this.chainName, expectedConfig);
const implementation = await deployer.deployContractWithName(this.chainName, expectedConfig.type, hypERC20contracts[expectedConfig.type], constructorArgs, undefined, false);
const provider = this.multiProvider.getProvider(this.domainId);
const proxyAddress = this.args.addresses.deployedTokenRoute;
const proxyAdminAddress = await proxyAdmin(provider, proxyAddress);
assert(await isInitialized(provider, proxyAddress), 'Proxy is not initialized');
updateTransactions.push({
chainId: this.chainId,
annotation: `Upgrading Warp Route implementation on ${this.args.chain}`,
to: proxyAdminAddress,
data: ProxyAdmin__factory.createInterface().encodeFunctionData('upgrade', [proxyAddress, implementation.address]),
});
return 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);
const actualConfig = await warpModule.read();
if (config.remoteRouters && !isObjEmpty(config.remoteRouters)) {
const enrollRemoteTxs = await warpModule.createEnrollRemoteRoutersUpdateTxs(actualConfig, config); // @TODO Remove when EvmERC20WarpModule.create can be used
const onlyTxIndex = 0;
await multiProvider.sendTransaction(chain, enrollRemoteTxs[onlyTxIndex]);
}
if (isMovableCollateralTokenConfig(config) &&
config.allowedRebalancers &&
config.allowedRebalancers.length !== 0) {
const addRebalancerTxs = await warpModule.createAddRebalancersUpdateTxs(actualConfig, config); // @TODO Remove when EvmERC20WarpModule.create can be used
for (const tx of addRebalancerTxs) {
await multiProvider.sendTransaction(chain, tx);
}
}
if (isMovableCollateralTokenConfig(config) &&
config.allowedRebalancingBridges &&
!isObjEmpty(config.allowedRebalancingBridges)) {
const addBridgesTxs = await warpModule.createAddAllowedBridgesUpdateTxs(actualConfig, config); // @TODO Remove when EvmERC20WarpModule.create can be used
for (const tx of addBridgesTxs) {
await multiProvider.sendTransaction(chain, tx);
}
}
return warpModule;
}
}
//# sourceMappingURL=EvmERC20WarpModule.js.map