feat: allow update destination gas (#4674)

### Description
Updates destination gas on all chain by getting the value from
warpConfig.gas. It then synchronizes all destination gas amounts across
all chains.


### Related issues
- Fixes #4529 


### Backward compatibility

Yes

### Testing

Manual/e2e
pull/4788/head
Lee 3 weeks ago committed by GitHub
parent 1b5bfa3466
commit 7e9e248bef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/long-queens-deny.md
  2. 2
      .changeset/tidy-meals-add.md
  3. 151
      typescript/cli/src/deploy/warp.ts
  4. 56
      typescript/cli/src/tests/warp-apply.e2e-test.ts
  5. 1
      typescript/sdk/src/index.ts
  6. 7
      typescript/sdk/src/router/schemas.ts
  7. 2
      typescript/sdk/src/router/types.ts
  8. 34
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  9. 62
      typescript/sdk/src/token/EvmERC20WarpModule.ts
  10. 28
      typescript/sdk/src/token/EvmERC20WarpRouteReader.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add feat to allow updates to destination gas using warp apply

@ -1,5 +1,5 @@
---
"@hyperlane-xyz/utils": patch
'@hyperlane-xyz/utils': patch
---
Filter undefined/null values in invertKeysAndValues function

@ -12,6 +12,7 @@ import {
ChainSubmissionStrategy,
ChainSubmissionStrategySchema,
ContractVerifier,
DestinationGas,
EvmERC20WarpModule,
EvmERC20WarpRouteReader,
EvmIsmModule,
@ -54,7 +55,6 @@ import {
Address,
ProtocolType,
assert,
isObjEmpty,
objFilter,
objKeys,
objMap,
@ -474,7 +474,6 @@ async function extendWarpRoute(
warpDeployConfig: WarpRouteDeployConfig,
warpCoreConfigByChain: ChainMap<WarpCoreConfig['tokens'][number]>,
) {
logBlue('Extending Warp Route');
const { multiProvider } = params.context;
const warpCoreChains = Object.keys(warpCoreConfigByChain);
@ -489,7 +488,10 @@ async function extendWarpRoute(
(chain, _config): _config is any => !warpCoreChains.includes(chain),
);
if (isObjEmpty(extendedConfigs)) return [];
const extendedChains = Object.keys(extendedConfigs);
if (extendedChains.length === 0) return [];
logBlue(`Extending Warp Route to ${extendedChains.join(', ')}`);
extendedConfigs = await deriveMetadataFromExisting(
multiProvider,
@ -536,28 +538,33 @@ async function updateExistingWarpRoute(
ExplorerLicenseType.MIT,
);
const transactions: AnnotatedEV5Transaction[] = [];
await promiseObjAll(
objMap(warpDeployConfig, async (chain, config) => {
const deployedConfig = warpCoreConfigByChain[chain];
if (!deployedConfig)
return logGray(
`Missing artifacts for ${chain}. Probably new deployment. Skipping update...`,
);
config.ismFactoryAddresses = addresses[
chain
] as ProxyFactoryFactoriesAddresses;
const evmERC20WarpModule = new EvmERC20WarpModule(
multiProvider,
{
config,
chain,
addresses: {
deployedTokenRoute: deployedConfig.addressOrDenom!,
await retryAsync(async () => {
logGray(`Update existing warp route for chain ${chain}`);
const deployedConfig = warpCoreConfigByChain[chain];
if (!deployedConfig)
return logGray(
`Missing artifacts for ${chain}. Probably new deployment. Skipping update...`,
);
config.ismFactoryAddresses = addresses[
chain
] as ProxyFactoryFactoriesAddresses;
const evmERC20WarpModule = new EvmERC20WarpModule(
multiProvider,
{
config,
chain,
addresses: {
deployedTokenRoute: deployedConfig.addressOrDenom!,
},
},
},
contractVerifier,
);
transactions.push(...(await evmERC20WarpModule.update(config)));
contractVerifier,
);
transactions.push(...(await evmERC20WarpModule.update(config)));
});
}),
);
return transactions;
@ -636,11 +643,17 @@ async function enrollRemoteRouters(
): Promise<AnnotatedEV5Transaction[]> {
logBlue(`Enrolling deployed routers with each other...`);
const { multiProvider } = params.context;
const deployedRouters: ChainMap<Address> = objMap(
const deployedRoutersAddresses: ChainMap<Address> = objMap(
deployedContractsMap,
(_, contracts) => getRouter(contracts).address,
);
const allChains = Object.keys(deployedRouters);
const deployedDestinationGas: DestinationGas = await populateDestinationGas(
multiProvider,
params.warpDeployConfig,
deployedContractsMap,
);
const deployedChains = Object.keys(deployedRoutersAddresses);
const transactions: AnnotatedEV5Transaction[] = [];
await promiseObjAll(
objMap(deployedContractsMap, async (chain, contracts) => {
@ -662,14 +675,23 @@ async function enrollRemoteRouters(
const otherChains = multiProvider
.getRemoteChains(chain)
.filter((c) => allChains.includes(c));
.filter((c) => deployedChains.includes(c));
mutatedWarpRouteConfig.remoteRouters =
otherChains.reduce<RemoteRouters>((remoteRouters, chain) => {
remoteRouters[multiProvider.getDomainId(chain)] =
deployedRouters[chain];
otherChains.reduce<RemoteRouters>((remoteRouters, otherChain) => {
remoteRouters[multiProvider.getDomainId(otherChain)] =
deployedRoutersAddresses[otherChain];
return remoteRouters;
}, {});
mutatedWarpRouteConfig.destinationGas =
otherChains.reduce<DestinationGas>((destinationGas, otherChain) => {
const otherChainDomain = multiProvider.getDomainId(otherChain);
destinationGas[otherChainDomain] =
deployedDestinationGas[otherChainDomain];
return destinationGas;
}, {});
const mutatedConfigTxs: AnnotatedEV5Transaction[] =
await evmERC20WarpModule.update(mutatedWarpRouteConfig);
@ -685,6 +707,38 @@ async function enrollRemoteRouters(
return transactions;
}
/**
* Populates the destination gas amounts for each chain using warpConfig.gas OR querying other router's destinationGas
*/
async function populateDestinationGas(
multiProvider: MultiProvider,
warpDeployConfig: WarpRouteDeployConfig,
deployedContractsMap: HyperlaneContractsMap<HypERC20Factories>,
): Promise<DestinationGas> {
const destinationGas: DestinationGas = {};
const deployedChains = Object.keys(deployedContractsMap);
await promiseObjAll(
objMap(deployedContractsMap, async (chain, contracts) => {
await retryAsync(async () => {
const router = getRouter(contracts);
const otherChains = multiProvider
.getRemoteChains(chain)
.filter((c) => deployedChains.includes(c));
for (const otherChain of otherChains) {
const otherDomain = multiProvider.getDomainId(otherChain);
if (!destinationGas[otherDomain])
destinationGas[otherDomain] =
warpDeployConfig[otherChain].gas?.toString() ||
(await router.destinationGas(otherDomain)).toString();
}
});
}),
);
return destinationGas;
}
function getRouter(contracts: HyperlaneContracts<HypERC20Factories>) {
for (const key of objKeys(hypERC20factories)) {
if (contracts[key]) return contracts[key];
@ -830,24 +884,29 @@ async function submitWarpApplyTransactions(
const { multiProvider } = params.context;
await promiseObjAll(
objMap(chainTransactions, async (chainId, transactions) => {
const chain = multiProvider.getChainName(chainId);
const submitter: TxSubmitterBuilder<ProtocolType> =
await getWarpApplySubmitter({
chain,
context: params.context,
strategyUrl: params.strategyUrl,
});
const transactionReceipts = await submitter.submit(...transactions);
if (transactionReceipts) {
const receiptPath = `${params.receiptsDir}/${chain}-${
submitter.txSubmitterType
}-${Date.now()}-receipts.json`;
writeYamlOrJson(receiptPath, transactionReceipts);
logGreen(
`Transactions receipts successfully written to ${receiptPath}`,
);
}
await retryAsync(
async () => {
const chain = multiProvider.getChainName(chainId);
const submitter: TxSubmitterBuilder<ProtocolType> =
await getWarpApplySubmitter({
chain,
context: params.context,
strategyUrl: params.strategyUrl,
});
const transactionReceipts = await submitter.submit(...transactions);
if (transactionReceipts) {
const receiptPath = `${params.receiptsDir}/${chain}-${
submitter.txSubmitterType
}-${Date.now()}-receipts.json`;
writeYamlOrJson(receiptPath, transactionReceipts);
logGreen(
`Transactions receipts successfully written to ${receiptPath}`,
);
}
},
5, // attempts
100, // baseRetryMs
);
}),
);
}

@ -259,4 +259,60 @@ describe('WarpApply e2e tests', async function () {
expect(remoteRouterKeys2).to.include(chain3Id);
expect(remoteRouterKeys3).to.include(chain2Id);
});
it('should extend an existing warp route and update all destination domains', async () => {
// Read existing config into a file
const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`;
const warpDeployConfig = await readWarpConfig(
CHAIN_NAME_2,
WARP_CORE_CONFIG_PATH_2,
warpConfigPath,
);
warpDeployConfig[CHAIN_NAME_2].gas = 7777;
// Extend with new config
const GAS = 694200;
const extendedConfig: TokenRouterConfig = {
decimals: 18,
mailbox: chain2Addresses!.mailbox,
name: 'Ether',
owner: new Wallet(ANVIL_KEY).address,
symbol: 'ETH',
totalSupply: 0,
type: TokenType.native,
gas: GAS,
};
warpDeployConfig[CHAIN_NAME_3] = extendedConfig;
writeYamlOrJson(warpConfigPath, warpDeployConfig);
await hyperlaneWarpApply(warpConfigPath, WARP_CORE_CONFIG_PATH_2);
const COMBINED_WARP_CORE_CONFIG_PATH = `${REGISTRY_PATH}/deployments/warp_routes/ETH/anvil2-anvil3-config.yaml`;
// Check that chain2 is enrolled in chain1
const updatedWarpDeployConfig_2 = await readWarpConfig(
CHAIN_NAME_2,
COMBINED_WARP_CORE_CONFIG_PATH,
warpConfigPath,
);
const chain2Id = await getChainId(CHAIN_NAME_2, ANVIL_KEY);
const chain3Id = await getChainId(CHAIN_NAME_3, ANVIL_KEY);
// Destination gas should be set in the existing chain (chain2) to include the extended chain (chain3)
const destinationGas_2 =
updatedWarpDeployConfig_2[CHAIN_NAME_2].destinationGas!;
expect(Object.keys(destinationGas_2)).to.include(chain3Id);
expect(destinationGas_2[chain3Id]).to.equal(GAS.toString());
// Destination gas should be set for the extended chain (chain3)
const updatedWarpDeployConfig_3 = await readWarpConfig(
CHAIN_NAME_3,
COMBINED_WARP_CORE_CONFIG_PATH,
warpConfigPath,
);
const destinationGas_3 =
updatedWarpDeployConfig_3[CHAIN_NAME_3].destinationGas!;
expect(Object.keys(destinationGas_3)).to.include(chain2Id);
expect(destinationGas_3[chain2Id]).to.equal('7777');
});
});

@ -394,6 +394,7 @@ export {
ProxiedFactories,
ProxiedRouterConfig,
RemoteRouters,
DestinationGas,
RouterAddress,
RouterConfig,
RouterViolation,

@ -32,6 +32,13 @@ export const RouterConfigSchema = MailboxClientConfigSchema.merge(
}),
);
const DestinationGasDomain = z.string();
const DestinationGasAmount = z.string(); // This must be a string type to match Ether's type
export const DestinationGasSchema = z.record(
DestinationGasDomain,
DestinationGasAmount,
);
export const GasRouterConfigSchema = RouterConfigSchema.extend({
gas: z.number().optional(),
destinationGas: DestinationGasSchema.optional(),
});

@ -14,6 +14,7 @@ import { CheckerViolation } from '../deploy/types.js';
import { ChainMap } from '../types.js';
import {
DestinationGasSchema,
GasRouterConfigSchema,
MailboxClientConfigSchema,
RemoteRoutersSchema,
@ -66,3 +67,4 @@ export interface RouterViolation extends CheckerViolation {
}
export type RemoteRouters = z.infer<typeof RemoteRoutersSchema>;
export type DestinationGas = z.infer<typeof DestinationGasSchema>;

@ -518,5 +518,39 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
});
expect(txs.length).to.equal(0);
});
it('should update the destination gas', async () => {
const domain = 3;
const config: TokenRouterConfig = {
...baseConfig,
type: TokenType.native,
hook: hookAddress,
ismFactoryAddresses,
remoteRouters: {
[domain]: randomAddress(),
},
};
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config: {
...config,
},
multiProvider,
});
await sendTxs(
await evmERC20WarpModule.update({
...config,
destinationGas: {
[domain]: '5000',
},
}),
);
const updatedConfig = await evmERC20WarpModule.read();
expect(Object.keys(updatedConfig.destinationGas!).length).to.be.equal(1);
expect(updatedConfig.destinationGas![domain]).to.equal('5000');
});
});
});

@ -1,4 +1,7 @@
import { BigNumberish } from 'ethers';
import {
GasRouter__factory,
MailboxClient__factory,
TokenRouter__factory,
} from '@hyperlane-xyz/core';
@ -12,6 +15,7 @@ import {
assert,
deepEquals,
isObjEmpty,
objMap,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -93,9 +97,16 @@ export class EvmERC20WarpModule extends HyperlaneModule<
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)),
...this.createRemoteRoutersUpdateTxs(actualConfig, expectedConfig),
...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig),
...this.createOwnershipUpdateTxs(actualConfig, expectedConfig),
);
@ -153,6 +164,57 @@ export class EvmERC20WarpModule extends HyperlaneModule<
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: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): AnnotatedEV5Transaction[] {
const updateTransactions: AnnotatedEV5Transaction[] = [];
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: { domain: BigNumberish; gas: BigNumberish }[] =
[];
objMap(expectedDestinationGas, (domain: string, gas: string) => {
gasRouterConfigs.push({
domain,
gas,
});
});
updateTransactions.push({
annotation: `Setting destination gas for ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
chainId: this.domainId,
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
*

@ -25,7 +25,7 @@ import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
import { EvmHookReader } from '../hook/EvmHookReader.js';
import { EvmIsmReader } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { RemoteRouters } from '../router/types.js';
import { DestinationGas, RemoteRouters } from '../router/types.js';
import { ChainNameOrId } from '../types.js';
import { HyperlaneReader } from '../utils/HyperlaneReader.js';
@ -64,11 +64,13 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader {
const baseMetadata = await this.fetchMailboxClientConfig(warpRouteAddress);
const tokenMetadata = await this.fetchTokenMetadata(type, warpRouteAddress);
const remoteRouters = await this.fetchRemoteRouters(warpRouteAddress);
const destinationGas = await this.fetchDestinationGas(warpRouteAddress);
return {
...baseMetadata,
...tokenMetadata,
remoteRouters,
destinationGas,
type,
} as TokenRouterConfig;
}
@ -245,4 +247,28 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader {
),
);
}
async fetchDestinationGas(
warpRouteAddress: Address,
): Promise<DestinationGas> {
const warpRoute = TokenRouter__factory.connect(
warpRouteAddress,
this.provider,
);
/**
* @remark
* Router.domains() is used to enumerate the destination gas because GasRouter.destinationGas is not EnumerableMapExtended type
* This means that if a domain is removed, then we cannot read the destinationGas for it. This may impact updates.
*/
const domains = await warpRoute.domains();
return Object.fromEntries(
await Promise.all(
domains.map(async (domain) => {
return [domain, (await warpRoute.destinationGas(domain)).toString()];
}),
),
);
}
}

Loading…
Cancel
Save