From 95fdc5f79383846d48158d05363a10893a901526 Mon Sep 17 00:00:00 2001 From: Paul Balaji <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:44:02 +0100 Subject: [PATCH 01/26] chore: update agents with reorg remediations (#4686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description chore: update agents with reorg remediations replaces https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4685 ### Drive-by changes na ### Related issues na ### Backward compatibility yes ### Testing 👁️ --------- Signed-off-by: pbio <10051819+paulbalaji@users.noreply.github.com> --- typescript/infra/config/environments/mainnet3/agent.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 40464a562..6e34d70cf 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -418,7 +418,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '18c29c8-20241014-133718', + tag: 'efd438f-20241016-101828', }, gasPaymentEnforcement: gasPaymentEnforcement, metricAppContexts, @@ -427,7 +427,7 @@ const hyperlane: RootAgentConfig = { validators: { docker: { repo, - tag: '18c29c8-20241014-133718', + tag: 'efd438f-20241016-101828', }, rpcConsensusType: RpcConsensusType.Quorum, chains: validatorChainConfig(Contexts.Hyperlane), @@ -437,7 +437,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '18c29c8-20241014-133718', + tag: 'efd438f-20241016-101828', }, resources: scraperResources, }, From 777ef084a67f864c529757a250e1c3898b470b03 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:01:30 +0100 Subject: [PATCH 02/26] fix: queue length metric bug (#4691) ### Description Fix for https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4689, where `Dropped` status operations weren't decremented from the metric. I noticed there's still a small difference on a handful of chains, such as celo, where even if Dropped ones are considered, the prep queue metric reports an extra 53 ops compared to the actual queue. Could be that I manually miscounted, I expect releasing this will be a big improvement anyway. ### Backward compatibility Yes ### Testing No, just checked all places where we `.pop_many` but don't `push` --- rust/main/agents/relayer/src/msg/op_queue.rs | 10 ++++------ rust/main/agents/relayer/src/msg/op_submitter.rs | 7 ++++--- .../hyperlane-core/src/traits/pending_operation.rs | 13 +++++++++++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/rust/main/agents/relayer/src/msg/op_queue.rs b/rust/main/agents/relayer/src/msg/op_queue.rs index 41ac5b6d6..d22e0c3c8 100644 --- a/rust/main/agents/relayer/src/msg/op_queue.rs +++ b/rust/main/agents/relayer/src/msg/op_queue.rs @@ -29,12 +29,10 @@ impl OpQueue { /// it's very likely that its status has just changed, so this forces the caller to consider the new status #[instrument(skip(self), ret, fields(queue_label=%self.queue_metrics_label), level = "trace")] pub async fn push(&self, mut op: QueueOperation, new_status: Option) { - if let Some(new_status) = new_status { - op.set_status_and_update_metrics( - new_status, - Arc::new(self.get_operation_metric(op.as_ref())), - ); - } + op.set_status_and_update_metrics( + new_status, + Arc::new(self.get_operation_metric(op.as_ref())), + ); self.queue.lock().await.push(Reverse(op)); } diff --git a/rust/main/agents/relayer/src/msg/op_submitter.rs b/rust/main/agents/relayer/src/msg/op_submitter.rs index 0694595ae..f35a991c4 100644 --- a/rust/main/agents/relayer/src/msg/op_submitter.rs +++ b/rust/main/agents/relayer/src/msg/op_submitter.rs @@ -293,6 +293,7 @@ async fn prepare_task( } PendingOperationResult::Drop => { metrics.ops_dropped.inc(); + op.decrement_metric_if_exists(); } PendingOperationResult::Confirm(reason) => { debug!(?op, "Pushing operation to confirm queue"); @@ -369,6 +370,7 @@ async fn submit_single_operation( } PendingOperationResult::Drop => { // Not expected to hit this case in `submit`, but it's here for completeness + op.decrement_metric_if_exists(); } PendingOperationResult::Success | PendingOperationResult::Confirm(_) => { confirm_op(op, confirm_queue, metrics).await @@ -457,9 +459,7 @@ async fn confirm_operation( PendingOperationResult::Success => { debug!(?op, "Operation confirmed"); metrics.ops_confirmed.inc(); - if let Some(metric) = op.get_metric() { - metric.dec() - } + op.decrement_metric_if_exists(); } PendingOperationResult::NotReady => { confirm_queue.push(op, None).await; @@ -478,6 +478,7 @@ async fn confirm_operation( } PendingOperationResult::Drop => { metrics.ops_dropped.inc(); + op.decrement_metric_if_exists(); } } operation_result diff --git a/rust/main/hyperlane-core/src/traits/pending_operation.rs b/rust/main/hyperlane-core/src/traits/pending_operation.rs index 8906777c3..f5480b197 100644 --- a/rust/main/hyperlane-core/src/traits/pending_operation.rs +++ b/rust/main/hyperlane-core/src/traits/pending_operation.rs @@ -67,6 +67,13 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs { /// Get the metric associated with this operation. fn get_metric(&self) -> Option>; + /// Decrement the metric associated with this operation if it exists. + fn decrement_metric_if_exists(&self) { + if let Some(metric) = self.get_metric() { + metric.dec(); + } + } + /// Set the metric associated with this operation. fn set_metric(&mut self, metric: Arc); @@ -80,10 +87,12 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs { /// Set the status of the operation and update the metrics. fn set_status_and_update_metrics( &mut self, - status: PendingOperationStatus, + status: Option, new_metric: Arc, ) { - self.set_status(status); + if let Some(status) = status { + self.set_status(status); + } if let Some(old_metric) = self.get_metric() { old_metric.dec(); } From b1ff48bd1d34e0ab2b0d9419d3767c53c469fcae Mon Sep 17 00:00:00 2001 From: Lee <6251863+ltyu@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:18:06 -0400 Subject: [PATCH 03/26] feat(cli,sdk): Add rebase yield route support (#4474) ### Description This PR adds support for **Rebase Collateral Vault** and **Synthetic Rebase** into the SDK and CLI. The SDK enforces a single Rebase Collateral Vault **must** be deployed with Synthetic Rebase via schema validation and transformation. The CLI filters the subsequent token list depending on the selection. ### Related issues - Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4512 ### Backward compatibility Yes ### Testing Manual/Unit Tests - Manually test deployment - E2E test for deployment and message sending --- .changeset/plenty-chicken-clean.md | 6 + typescript/cli/package.json | 4 +- typescript/cli/src/config/warp.ts | 40 +++++- typescript/cli/src/send/transfer.ts | 6 +- typescript/cli/src/tests/commands/helpers.ts | 58 ++++++++- typescript/cli/src/tests/commands/warp.ts | 21 ++++ .../cli/src/tests/warp-apply.e2e-test.ts | 2 +- .../cli/src/tests/warp-deploy.e2e-test.ts | 114 +++++++++++++++++ typescript/sdk/src/index.ts | 2 + .../token/EvmERC20WarpModule.hardhat-test.ts | 7 +- .../EvmERC20WarpRouteReader.hardhat-test.ts | 110 +++++++++++++++-- .../sdk/src/token/EvmERC20WarpRouteReader.ts | 25 +++- typescript/sdk/src/token/Token.test.ts | 17 +++ typescript/sdk/src/token/Token.ts | 20 +-- typescript/sdk/src/token/TokenStandard.ts | 8 ++ typescript/sdk/src/token/config.ts | 3 + typescript/sdk/src/token/contracts.ts | 10 +- typescript/sdk/src/token/deploy.ts | 8 +- typescript/sdk/src/token/schemas.test.ts | 116 ++++++++++++++++-- typescript/sdk/src/token/schemas.ts | 70 ++++++++++- yarn.lock | 33 ++++- 21 files changed, 628 insertions(+), 52 deletions(-) create mode 100644 .changeset/plenty-chicken-clean.md create mode 100644 typescript/cli/src/tests/warp-deploy.e2e-test.ts diff --git a/.changeset/plenty-chicken-clean.md b/.changeset/plenty-chicken-clean.md new file mode 100644 index 000000000..e35a59eff --- /dev/null +++ b/.changeset/plenty-chicken-clean.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Add rebasing yield route support into CLI/SDK diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 19421e2e9..eb1403de0 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -25,12 +25,14 @@ "devDependencies": { "@ethersproject/abi": "*", "@ethersproject/providers": "*", + "@types/chai-as-promised": "^8", "@types/mocha": "^10.0.1", "@types/node": "^18.14.5", "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", - "chai": "4.5.0", + "chai": "^4.5.0", + "chai-as-promised": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "mocha": "^10.2.0", diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 6ce1f6665..29246cd33 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -34,12 +34,15 @@ import { createAdvancedIsmConfig } from './ism.js'; const TYPE_DESCRIPTIONS: Record = { [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', + [TokenType.syntheticRebase]: `A rebasing ERC20 with remote transfer functionality. Must be paired with ${TokenType.collateralVaultRebase}`, [TokenType.collateral]: 'Extends an existing ERC20 with remote transfer functionality', [TokenType.native]: 'Extends the native token with remote transfer functionality', [TokenType.collateralVault]: - 'Extends an existing ERC4626 with remote transfer functionality', + 'Extends an existing ERC4626 with remote transfer functionality. Yields are manually claimed by owner.', + [TokenType.collateralVaultRebase]: + 'Extends an existing ERC4626 with remote transfer functionality. Rebases yields to token holders.', [TokenType.collateralFiat]: 'Extends an existing FiatToken with remote transfer functionality', [TokenType.XERC20]: @@ -129,6 +132,7 @@ export async function createWarpRouteDeployConfig({ ); const result: WarpRouteDeployConfig = {}; + let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); @@ -167,7 +171,7 @@ export async function createWarpRouteDeployConfig({ const type = await select({ message: `Select ${chain}'s token type`, - choices: TYPE_CHOICES, + choices: typeChoices, }); // TODO: restore NFT prompting @@ -192,6 +196,34 @@ export async function createWarpRouteDeployConfig({ }), }; break; + case TokenType.syntheticRebase: + result[chain] = { + mailbox, + type, + owner, + isNft, + collateralChainName: '', // This will be derived correctly by zod.parse() below + interchainSecurityModule, + }; + typeChoices = restrictChoices([ + TokenType.syntheticRebase, + TokenType.collateralVaultRebase, + ]); + break; + case TokenType.collateralVaultRebase: + result[chain] = { + mailbox, + type, + owner, + isNft, + interchainSecurityModule, + token: await input({ + message: `Enter the ERC-4626 vault address on chain ${chain}`, + }), + }; + + typeChoices = restrictChoices([TokenType.syntheticRebase]); + break; case TokenType.collateralVault: result[chain] = { mailbox, @@ -229,6 +261,10 @@ export async function createWarpRouteDeployConfig({ } } +function restrictChoices(typeChoices: TokenType[]) { + return TYPE_CHOICES.filter((choice) => typeChoices.includes(choice.name)); +} + // Note, this is different than the function above which reads a config // for a DEPLOYMENT. This gets a config for using a warp route (aka WarpCoreConfig) export function readWarpCoreConfig(filePath: string): WarpCoreConfig { diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index a2f56ef29..a89eb6aa9 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -23,6 +23,10 @@ import { indentYamlOrJson } from '../utils/files.js'; import { stubMerkleTreeConfig } from '../utils/relay.js'; import { runTokenSelectionStep } from '../utils/tokens.js'; +export const WarpSendLogs = { + SUCCESS: 'Transfer was self-relayed!', +}; + export async function sendTestTransfer({ context, warpCoreConfig, @@ -183,7 +187,7 @@ async function executeDelivery({ log('Attempting self-relay of transfer...'); await relayer.relayMessage(transferTxReceipt, messageIndex, message); - logGreen('Transfer was self-relayed!'); + logGreen(WarpSendLogs.SUCCESS); return; } diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index 2d2be7a6e..c4ad03651 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -1,3 +1,4 @@ +import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core'; import { ChainAddresses } from '@hyperlane-xyz/registry'; import { TokenRouterConfig, @@ -10,7 +11,11 @@ import { getContext } from '../../context/context.js'; import { readYamlOrJson, writeYamlOrJson } from '../../utils/files.js'; import { hyperlaneCoreDeploy } from './core.js'; -import { hyperlaneWarpApply, readWarpConfig } from './warp.js'; +import { + hyperlaneWarpApply, + hyperlaneWarpSendRelay, + readWarpConfig, +} from './warp.js'; export const TEST_CONFIGS_PATH = './test-configs'; export const REGISTRY_PATH = `${TEST_CONFIGS_PATH}/anvil`; @@ -123,3 +128,54 @@ export async function getChainId(chainName: string, key: string) { const chainMetadata = await registry.getChainMetadata(chainName); return String(chainMetadata?.chainId); } + +export async function deployToken(privateKey: string, chain: string) { + const { multiProvider } = await getContext({ + registryUri: REGISTRY_PATH, + registryOverrideUri: '', + key: privateKey, + }); + + const token = await new ERC20Test__factory( + multiProvider.getSigner(chain), + ).deploy('token', 'token', '100000000000000000000', 18); + await token.deployed(); + + return token; +} + +export async function deploy4626Vault( + privateKey: string, + chain: string, + tokenAddress: string, +) { + const { multiProvider } = await getContext({ + registryUri: REGISTRY_PATH, + registryOverrideUri: '', + key: privateKey, + }); + + const vault = await new ERC4626Test__factory( + multiProvider.getSigner(chain), + ).deploy(tokenAddress, 'VAULT', 'VAULT'); + await vault.deployed(); + + return vault; +} + +/** + * Performs a round-trip warp relay between two chains using the specified warp core config. + * + * @param chain1 - The first chain to send the warp relay from. + * @param chain2 - The second chain to send the warp relay to and back from. + * @param warpCoreConfigPath - The path to the warp core config file. + * @returns A promise that resolves when the round-trip warp relay is complete. + */ +export async function sendWarpRouteMessageRoundTrip( + chain1: string, + chain2: string, + warpCoreConfigPath: string, +) { + await hyperlaneWarpSendRelay(chain1, chain2, warpCoreConfigPath); + return hyperlaneWarpSendRelay(chain2, chain1, warpCoreConfigPath); +} diff --git a/typescript/cli/src/tests/commands/warp.ts b/typescript/cli/src/tests/commands/warp.ts index 16b8acbfe..3f3eec338 100644 --- a/typescript/cli/src/tests/commands/warp.ts +++ b/typescript/cli/src/tests/commands/warp.ts @@ -12,8 +12,10 @@ $.verbose = true; * Deploys the Warp route to the specified chain using the provided config. */ export async function hyperlaneWarpDeploy(warpCorePath: string) { + // --overrides is " " to allow local testing to work return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \ --registry ${REGISTRY_PATH} \ + --overrides " " \ --config ${warpCorePath} \ --key ${ANVIL_KEY} \ --verbosity debug \ @@ -30,6 +32,7 @@ export async function hyperlaneWarpApply( ) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp apply \ --registry ${REGISTRY_PATH} \ + --overrides " " \ --config ${warpDeployPath} \ --warp ${warpCorePath} \ --key ${ANVIL_KEY} \ @@ -45,6 +48,7 @@ export async function hyperlaneWarpRead( ) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp read \ --registry ${REGISTRY_PATH} \ + --overrides " " \ --address ${warpAddress} \ --chain ${chain} \ --key ${ANVIL_KEY} \ @@ -52,6 +56,23 @@ export async function hyperlaneWarpRead( --config ${warpDeployPath}`; } +export async function hyperlaneWarpSendRelay( + origin: string, + destination: string, + warpCorePath: string, +) { + return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp send \ + --relay \ + --registry ${REGISTRY_PATH} \ + --overrides " " \ + --origin ${origin} \ + --destination ${destination} \ + --warp ${warpCorePath} \ + --key ${ANVIL_KEY} \ + --verbosity debug \ + --yes`; +} + /** * Reads the Warp route deployment config to specified output path. * @param warpCorePath path to warp core diff --git a/typescript/cli/src/tests/warp-apply.e2e-test.ts b/typescript/cli/src/tests/warp-apply.e2e-test.ts index 7b0c20d91..2346038d6 100644 --- a/typescript/cli/src/tests/warp-apply.e2e-test.ts +++ b/typescript/cli/src/tests/warp-apply.e2e-test.ts @@ -36,7 +36,7 @@ const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh const WARP_CONFIG_PATH_2 = `${TEMP_PATH}/anvil2/warp-route-deployment-anvil2.yaml`; const WARP_CORE_CONFIG_PATH_2 = `${REGISTRY_PATH}/deployments/warp_routes/ETH/anvil2-config.yaml`; -const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while +const TEST_TIMEOUT = 100_000; // Long timeout since these tests can take a while describe('WarpApply e2e tests', async function () { let chain2Addresses: ChainAddresses = {}; this.timeout(TEST_TIMEOUT); diff --git a/typescript/cli/src/tests/warp-deploy.e2e-test.ts b/typescript/cli/src/tests/warp-deploy.e2e-test.ts new file mode 100644 index 000000000..6263f70cb --- /dev/null +++ b/typescript/cli/src/tests/warp-deploy.e2e-test.ts @@ -0,0 +1,114 @@ +import * as chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { ChainAddresses } from '@hyperlane-xyz/registry'; +import { TokenType, WarpRouteDeployConfig } from '@hyperlane-xyz/sdk'; + +import { WarpSendLogs } from '../send/transfer.js'; +import { writeYamlOrJson } from '../utils/files.js'; + +import { + ANVIL_KEY, + REGISTRY_PATH, + deploy4626Vault, + deployOrUseExistingCore, + deployToken, + sendWarpRouteMessageRoundTrip, +} from './commands/helpers.js'; +import { hyperlaneWarpDeploy, readWarpConfig } from './commands/warp.js'; + +chai.use(chaiAsPromised); +const expect = chai.expect; +chai.should(); + +const CHAIN_NAME_2 = 'anvil2'; +const CHAIN_NAME_3 = 'anvil3'; + +const EXAMPLES_PATH = './examples'; +const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh + +const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config.yaml`; +const WARP_CONFIG_PATH = `${TEMP_PATH}/warp-route-deployment-deploy.yaml`; +const WARP_CORE_CONFIG_PATH_2_3 = `${REGISTRY_PATH}/deployments/warp_routes/VAULT/anvil2-anvil3-config.yaml`; + +const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while +describe('WarpDeploy e2e tests', async function () { + let chain2Addresses: ChainAddresses = {}; + let token: any; + let vault: any; + + this.timeout(TEST_TIMEOUT); + + before(async function () { + chain2Addresses = await deployOrUseExistingCore( + CHAIN_NAME_2, + CORE_CONFIG_PATH, + ANVIL_KEY, + ); + + await deployOrUseExistingCore(CHAIN_NAME_3, CORE_CONFIG_PATH, ANVIL_KEY); + + token = await deployToken(ANVIL_KEY, CHAIN_NAME_2); + vault = await deploy4626Vault(ANVIL_KEY, CHAIN_NAME_2, token.address); + }); + + it('should only allow rebasing yield route to be deployed with rebasing synthetic', async function () { + const warpConfig: WarpRouteDeployConfig = { + [CHAIN_NAME_2]: { + type: TokenType.collateralVaultRebase, + token: vault.address, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + }, + [CHAIN_NAME_3]: { + type: TokenType.synthetic, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + }, + }; + + writeYamlOrJson(WARP_CONFIG_PATH, warpConfig); + await hyperlaneWarpDeploy(WARP_CONFIG_PATH).should.be.rejected; // TODO: revisit this to figure out how to parse the error. + }); + + it(`should be able to bridge between ${TokenType.collateralVaultRebase} and ${TokenType.syntheticRebase}`, async function () { + const warpConfig: WarpRouteDeployConfig = { + [CHAIN_NAME_2]: { + type: TokenType.collateralVaultRebase, + token: vault.address, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + }, + [CHAIN_NAME_3]: { + type: TokenType.syntheticRebase, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + collateralChainName: CHAIN_NAME_2, + }, + }; + + writeYamlOrJson(WARP_CONFIG_PATH, warpConfig); + await hyperlaneWarpDeploy(WARP_CONFIG_PATH); + + // Check collateralRebase + const collateralRebaseConfig = ( + await readWarpConfig( + CHAIN_NAME_2, + WARP_CORE_CONFIG_PATH_2_3, + WARP_CONFIG_PATH, + ) + )[CHAIN_NAME_2]; + + expect(collateralRebaseConfig.type).to.equal( + TokenType.collateralVaultRebase, + ); + + // Try to send a transaction + const { stdout } = await sendWarpRouteMessageRoundTrip( + CHAIN_NAME_2, + CHAIN_NAME_3, + WARP_CORE_CONFIG_PATH_2_3, + ); + expect(stdout).to.include(WarpSendLogs.SUCCESS); + }); +}); diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index ee54c0409..e80ad1f3e 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -512,9 +512,11 @@ export { NativeConfig, TokenRouterConfigSchema, WarpRouteDeployConfigSchema, + WarpRouteDeployConfigSchemaErrors, isCollateralConfig, isNativeConfig, isSyntheticConfig, + isSyntheticRebaseConfig, isTokenMetadata, } from './token/schemas.js'; export { isCompliant } from './utils/schemas.js'; diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index 6db166c5b..467a150ad 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -111,7 +111,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }); it('should create with a collateral config', async () => { - const config = { + const config: TokenRouterConfig = { ...baseConfig, type: TokenType.collateral, token: token.address, @@ -139,7 +139,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { TOKEN_NAME, TOKEN_NAME, ); - const config = { + const config: TokenRouterConfig = { type: TokenType.collateralVault, token: vault.address, hook: hookAddress, @@ -172,9 +172,8 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }); it('should create with a synthetic config', async () => { - const config = { + const config: TokenRouterConfig = { type: TokenType.synthetic, - token: token.address, hook: hookAddress, name: TOKEN_NAME, symbol: TOKEN_NAME, diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts index f086f3b83..c6795fc6c 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts @@ -14,14 +14,16 @@ import { HyperlaneContractsMap, RouterConfig, TestChainName, - TokenRouterConfig, + WarpRouteDeployConfig, test3, } from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; import { TestCoreApp } from '../core/TestCoreApp.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap } from '../types.js'; @@ -122,12 +124,12 @@ describe('ERC20WarpRouterReader', async () => { it('should derive collateral config correctly', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.collateral, token: token.address, hook: await mailbox.defaultHook(), - interchainsecurityModule: await mailbox.defaultIsm(), + interchainSecurityModule: await mailbox.defaultIsm(), ...baseConfig, }, }; @@ -150,8 +152,10 @@ describe('ERC20WarpRouterReader', async () => { config[chain].hook as string, ), ); - // Check ism. should return undefined - expect(derivedConfig.interchainSecurityModule).to.be.undefined; + // Check ism + expect( + (derivedConfig.interchainSecurityModule as DerivedIsmConfig).address, + ).to.be.equal(await mailbox.defaultIsm()); // Check if token values matches if (derivedConfig.type === TokenType.collateral) { @@ -160,13 +164,45 @@ describe('ERC20WarpRouterReader', async () => { expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); } }); + it('should derive synthetic rebase config correctly', async () => { + // Create config + const config: WarpRouteDeployConfig = { + [chain]: { + type: TokenType.syntheticRebase, + collateralChainName: TestChainName.test4, + hook: await mailbox.defaultHook(), + name: TOKEN_NAME, + symbol: TOKEN_NAME, + decimals: TOKEN_DECIMALS, + totalSupply: TOKEN_SUPPLY, + ...baseConfig, + }, + }; + // Deploy with config + const warpRoute = await deployer.deploy(config); + + // Derive config and check if each value matches + const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig( + warpRoute[chain].syntheticRebase.address, + ); + for (const [key, value] of Object.entries(derivedConfig)) { + const deployedValue = (config[chain] as any)[key]; + if (deployedValue && typeof value === 'string') + expect(deployedValue).to.equal(value); + } + + // Check if token values matches + if (derivedConfig.type === TokenType.collateral) { + expect(derivedConfig.name).to.equal(TOKEN_NAME); + expect(derivedConfig.symbol).to.equal(TOKEN_NAME); + } + }); it('should derive synthetic config correctly', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.synthetic, - token: token.address, hook: await mailbox.defaultHook(), name: TOKEN_NAME, symbol: TOKEN_NAME, @@ -197,13 +233,13 @@ describe('ERC20WarpRouterReader', async () => { it('should derive native config correctly', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.native, hook: await mailbox.defaultHook(), ...baseConfig, }, - } as ChainMap; + }; // Deploy with config const warpRoute = await deployer.deploy(config); @@ -221,9 +257,61 @@ describe('ERC20WarpRouterReader', async () => { expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); }); + it('should derive collateral vault config correctly', async () => { + // Create config + const config: WarpRouteDeployConfig = { + [chain]: { + type: TokenType.collateralVault, + token: vault.address, + ...baseConfig, + }, + }; + // Deploy with config + const warpRoute = await deployer.deploy(config); + // Derive config and check if each value matches + const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig( + warpRoute[chain].collateralVault.address, + ); + + assert( + derivedConfig.type === TokenType.collateralVault, + 'Must be collateralVault', + ); + expect(derivedConfig.type).to.equal(config[chain].type); + expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); + expect(derivedConfig.owner).to.equal(config[chain].owner); + expect(derivedConfig.token).to.equal(token.address); + }); + + it('should derive rebase collateral vault config correctly', async () => { + // Create config + const config: WarpRouteDeployConfig = { + [chain]: { + type: TokenType.collateralVaultRebase, + token: vault.address, + ...baseConfig, + }, + }; + // Deploy with config + const warpRoute = await deployer.deploy(config); + // Derive config and check if each value matches + const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig( + warpRoute[chain].collateralVaultRebase.address, + ); + + assert( + derivedConfig.type === TokenType.collateralVaultRebase, + 'Must be collateralVaultRebase', + ); + expect(derivedConfig.type).to.equal(config[chain].type); + expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); + expect(derivedConfig.owner).to.equal(config[chain].owner); + expect(derivedConfig.token).to.equal(token.address); + }); + it('should return undefined if ism is not set onchain', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.collateral, token: token.address, @@ -246,7 +334,7 @@ describe('ERC20WarpRouterReader', async () => { // Create config const otherChain = TestChainName.test3; const otherChainMetadata = test3; - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.collateral, token: token.address, diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 2eb3838d5..8ffbe82cc 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -4,6 +4,8 @@ import { HypERC20Collateral__factory, HypERC20__factory, HypERC4626Collateral__factory, + HypERC4626OwnerCollateral__factory, + HypERC4626__factory, TokenRouter__factory, } from '@hyperlane-xyz/core'; import { @@ -81,15 +83,23 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { const contractTypes: Partial< Record > = { - collateralVault: { + [TokenType.collateralVaultRebase]: { factory: HypERC4626Collateral__factory, + method: 'NULL_RECIPIENT', + }, + [TokenType.collateralVault]: { + factory: HypERC4626OwnerCollateral__factory, method: 'vault', }, - collateral: { + [TokenType.collateral]: { factory: HypERC20Collateral__factory, method: 'wrappedToken', }, - synthetic: { + [TokenType.syntheticRebase]: { + factory: HypERC4626__factory, + method: 'collateralDomain', + }, + [TokenType.synthetic]: { factory: HypERC20__factory, method: 'decimals', }, @@ -106,11 +116,11 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { try { const warpRoute = factory.connect(warpRouteAddress, this.provider); await warpRoute[method](); - - this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger return tokenType as TokenType; } catch (e) { continue; + } finally { + this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger } } @@ -186,7 +196,10 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { await this.fetchERC20Metadata(token); return { name, symbol, decimals, totalSupply, token }; - } else if (type === TokenType.synthetic) { + } else if ( + type === TokenType.synthetic || + type === TokenType.syntheticRebase + ) { return this.fetchERC20Metadata(tokenAddress); } else if (type === TokenType.native) { const chainMetadata = this.multiProvider.getChainMetadata(this.chain); diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index cd232ebf7..1a6ac978a 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -48,6 +48,15 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'USDC', name: 'USDC', }, + [TokenStandard.EvmHypRebaseCollateral]: { + chainName: TestChainName.test3, + standard: TokenStandard.EvmHypRebaseCollateral, + addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131', + collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930', + decimals: 18, + symbol: 'USDC', + name: 'USDC', + }, [TokenStandard.EvmHypOwnerCollateral]: { chainName: TestChainName.test3, standard: TokenStandard.EvmHypOwnerCollateral, @@ -74,6 +83,14 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'USDC', name: 'USDC', }, + [TokenStandard.EvmHypSyntheticRebase]: { + chainName: TestChainName.test2, + standard: TokenStandard.EvmHypSyntheticRebase, + addressOrDenom: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147', + decimals: 6, + symbol: 'USDC', + name: 'USDC', + }, [TokenStandard.EvmHypXERC20]: { chainName: TestChainName.test2, standard: TokenStandard.EvmHypXERC20, diff --git a/typescript/sdk/src/token/Token.ts b/typescript/sdk/src/token/Token.ts index e2541db6b..527adc2b5 100644 --- a/typescript/sdk/src/token/Token.ts +++ b/typescript/sdk/src/token/Token.ts @@ -189,19 +189,19 @@ export class Token implements IToken { return new EvmHypNativeAdapter(chainName, multiProvider, { token: addressOrDenom, }); - } else if (standard === TokenStandard.EvmHypCollateral) { - return new EvmHypCollateralAdapter(chainName, multiProvider, { - token: addressOrDenom, - }); - } else if (standard === TokenStandard.EvmHypCollateralFiat) { - return new EvmHypCollateralAdapter(chainName, multiProvider, { - token: addressOrDenom, - }); - } else if (standard === TokenStandard.EvmHypOwnerCollateral) { + } else if ( + standard === TokenStandard.EvmHypCollateral || + standard === TokenStandard.EvmHypCollateralFiat || + standard === TokenStandard.EvmHypOwnerCollateral || + standard === TokenStandard.EvmHypRebaseCollateral + ) { return new EvmHypCollateralAdapter(chainName, multiProvider, { token: addressOrDenom, }); - } else if (standard === TokenStandard.EvmHypSynthetic) { + } else if ( + standard === TokenStandard.EvmHypSynthetic || + standard === TokenStandard.EvmHypSyntheticRebase + ) { return new EvmHypSyntheticAdapter(chainName, multiProvider, { token: addressOrDenom, }); diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 002501d32..ee434b777 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -15,8 +15,10 @@ export enum TokenStandard { EvmHypNative = 'EvmHypNative', EvmHypCollateral = 'EvmHypCollateral', EvmHypOwnerCollateral = 'EvmHypOwnerCollateral', + EvmHypRebaseCollateral = 'EvmHypRebaseCollateral', EvmHypCollateralFiat = 'EvmHypCollateralFiat', EvmHypSynthetic = 'EvmHypSynthetic', + EvmHypSyntheticRebase = 'EvmHypSyntheticRebase', EvmHypXERC20 = 'EvmHypXERC20', EvmHypXERC20Lockbox = 'EvmHypXERC20Lockbox', @@ -52,8 +54,10 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record = { EvmHypNative: ProtocolType.Ethereum, EvmHypCollateral: ProtocolType.Ethereum, EvmHypOwnerCollateral: ProtocolType.Ethereum, + EvmHypRebaseCollateral: ProtocolType.Ethereum, EvmHypCollateralFiat: ProtocolType.Ethereum, EvmHypSynthetic: ProtocolType.Ethereum, + EvmHypSyntheticRebase: ProtocolType.Ethereum, EvmHypXERC20: ProtocolType.Ethereum, EvmHypXERC20Lockbox: ProtocolType.Ethereum, @@ -114,7 +118,9 @@ export const TOKEN_HYP_STANDARDS = [ TokenStandard.EvmHypCollateral, TokenStandard.EvmHypCollateralFiat, TokenStandard.EvmHypOwnerCollateral, + TokenStandard.EvmHypRebaseCollateral, TokenStandard.EvmHypSynthetic, + TokenStandard.EvmHypSyntheticRebase, TokenStandard.EvmHypXERC20, TokenStandard.EvmHypXERC20Lockbox, TokenStandard.SealevelHypNative, @@ -148,9 +154,11 @@ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.XERC20]: TokenStandard.EvmHypXERC20, [TokenType.XERC20Lockbox]: TokenStandard.EvmHypXERC20Lockbox, [TokenType.collateralVault]: TokenStandard.EvmHypOwnerCollateral, + [TokenType.collateralVaultRebase]: TokenStandard.EvmHypRebaseCollateral, [TokenType.collateralUri]: TokenStandard.EvmHypCollateral, [TokenType.fastCollateral]: TokenStandard.EvmHypCollateral, [TokenType.synthetic]: TokenStandard.EvmHypSynthetic, + [TokenType.syntheticRebase]: TokenStandard.EvmHypSyntheticRebase, [TokenType.syntheticUri]: TokenStandard.EvmHypSynthetic, [TokenType.fastSynthetic]: TokenStandard.EvmHypSynthetic, [TokenType.nativeScaled]: TokenStandard.EvmHypNative, diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index a89264ee6..08fb750f2 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -1,9 +1,11 @@ export enum TokenType { synthetic = 'synthetic', + syntheticRebase = 'syntheticRebase', fastSynthetic = 'fastSynthetic', syntheticUri = 'syntheticUri', collateral = 'collateral', collateralVault = 'collateralVault', + collateralVaultRebase = 'collateralVaultRebase', XERC20 = 'xERC20', XERC20Lockbox = 'xERC20Lockbox', collateralFiat = 'collateralFiat', @@ -16,6 +18,7 @@ export enum TokenType { export const CollateralExtensions = [ TokenType.collateral, TokenType.collateralVault, + TokenType.collateralVaultRebase, ]; export const gasOverhead = (tokenType: TokenType): number => { diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index 281c084ff..a9ced3cb1 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -8,6 +8,8 @@ import { HypERC721URIStorage__factory, HypERC721__factory, HypERC4626Collateral__factory, + HypERC4626OwnerCollateral__factory, + HypERC4626__factory, HypFiatToken__factory, HypNativeScaled__factory, HypNative__factory, @@ -21,11 +23,13 @@ export const hypERC20contracts = { [TokenType.fastCollateral]: 'FastHypERC20Collateral', [TokenType.fastSynthetic]: 'FastHypERC20', [TokenType.synthetic]: 'HypERC20', + [TokenType.syntheticRebase]: 'HypERC4626', [TokenType.collateral]: 'HypERC20Collateral', [TokenType.collateralFiat]: 'HypFiatToken', [TokenType.XERC20]: 'HypXERC20', [TokenType.XERC20Lockbox]: 'HypXERC20Lockbox', - [TokenType.collateralVault]: 'HypERC20CollateralVaultDeposit', + [TokenType.collateralVault]: 'HypERC4626OwnerCollateral', + [TokenType.collateralVaultRebase]: 'HypERC4626Collateral', [TokenType.native]: 'HypNative', [TokenType.nativeScaled]: 'HypNativeScaled', }; @@ -36,7 +40,9 @@ export const hypERC20factories = { [TokenType.fastSynthetic]: new FastHypERC20__factory(), [TokenType.synthetic]: new HypERC20__factory(), [TokenType.collateral]: new HypERC20Collateral__factory(), - [TokenType.collateralVault]: new HypERC4626Collateral__factory(), + [TokenType.collateralVault]: new HypERC4626OwnerCollateral__factory(), + [TokenType.collateralVaultRebase]: new HypERC4626Collateral__factory(), + [TokenType.syntheticRebase]: new HypERC4626__factory(), [TokenType.collateralFiat]: new HypFiatToken__factory(), [TokenType.XERC20]: new HypXERC20__factory(), [TokenType.XERC20Lockbox]: new HypXERC20Lockbox__factory(), diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index 843f933f1..ee2a9a399 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -33,6 +33,7 @@ import { isCollateralConfig, isNativeConfig, isSyntheticConfig, + isSyntheticRebaseConfig, isTokenMetadata, } from './schemas.js'; import { TokenMetadata, WarpRouteDeployConfig } from './types.js'; @@ -64,6 +65,11 @@ abstract class TokenDeployer< } else if (isSyntheticConfig(config)) { assert(config.decimals, 'decimals is undefined for config'); // decimals must be defined by this point return [config.decimals, config.mailbox]; + } else if (isSyntheticRebaseConfig(config)) { + const collateralDomain = this.multiProvider.getDomainId( + config.collateralChainName, + ); + return [config.decimals, config.mailbox, collateralDomain]; } else { throw new Error('Unknown token type when constructing arguments'); } @@ -82,7 +88,7 @@ abstract class TokenDeployer< ]; if (isCollateralConfig(config) || isNativeConfig(config)) { return defaultArgs; - } else if (isSyntheticConfig(config)) { + } else if (isSyntheticConfig(config) || isSyntheticRebaseConfig(config)) { return [config.totalSupply, config.name, config.symbol, ...defaultArgs]; } else { throw new Error('Unknown collateral type when initializing arguments'); diff --git a/typescript/sdk/src/token/schemas.test.ts b/typescript/sdk/src/token/schemas.test.ts index 65f780cd0..1a8d606ea 100644 --- a/typescript/sdk/src/token/schemas.test.ts +++ b/typescript/sdk/src/token/schemas.test.ts @@ -1,8 +1,15 @@ import { expect } from 'chai'; import { ethers } from 'ethers'; +import { assert } from '@hyperlane-xyz/utils'; + import { TokenType } from './config.js'; -import { WarpRouteDeployConfigSchema } from './schemas.js'; +import { + WarpRouteDeployConfigSchema, + WarpRouteDeployConfigSchemaErrors, + isCollateralConfig, +} from './schemas.js'; +import { WarpRouteDeployConfig } from './types.js'; const SOME_ADDRESS = ethers.Wallet.createRandom().address; const COLLATERAL_TYPES = [ @@ -19,7 +26,7 @@ const NON_COLLATERAL_TYPES = [ ]; describe('WarpRouteDeployConfigSchema refine', () => { - let config: any; + let config: WarpRouteDeployConfig; beforeEach(() => { config = { arbitrum: { @@ -33,18 +40,24 @@ describe('WarpRouteDeployConfigSchema refine', () => { it('should require token type', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + + //@ts-ignore delete config.arbitrum.type; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); it('should require token address', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + + //@ts-ignore delete config.arbitrum.token; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); it('should require mailbox address', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + + //@ts-ignore delete config.arbitrum.mailbox; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); @@ -52,6 +65,9 @@ describe('WarpRouteDeployConfigSchema refine', () => { it('should throw if collateral type and token is empty', async () => { for (const type of COLLATERAL_TYPES) { config.arbitrum.type = type; + assert(isCollateralConfig(config.arbitrum), 'must be collateral'); + + //@ts-ignore config.arbitrum.token = undefined; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; @@ -61,13 +77,8 @@ describe('WarpRouteDeployConfigSchema refine', () => { } }); - it('should accept native type if token is empty', async () => { - config.arbitrum.type = TokenType.native; - config.arbitrum.token = undefined; - expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; - }); - it('should succeed if non-collateral type, token is empty, metadata is defined', async () => { + //@ts-ignore delete config.arbitrum.token; config.arbitrum.totalSupply = '0'; config.arbitrum.name = 'name'; @@ -81,4 +92,93 @@ describe('WarpRouteDeployConfigSchema refine', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; } }); + + it(`should throw if deploying rebasing collateral with anything other than ${TokenType.syntheticRebase}`, async () => { + config = { + arbitrum: { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + ethereum: { + type: TokenType.collateralVault, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + optimism: { + type: TokenType.syntheticRebase, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + collateralChainName: '', + }, + }; + let parseResults = WarpRouteDeployConfigSchema.safeParse(config); + assert(!parseResults.success, 'must be false'); // Needed so 'message' shows up because parseResults is a discriminate union + expect(parseResults.error.issues[0].message).to.equal( + WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE, + ); + + config.ethereum.type = TokenType.syntheticRebase; + //@ts-ignore + config.ethereum.collateralChainName = ''; + parseResults = WarpRouteDeployConfigSchema.safeParse(config); + //@ts-ignore + expect(parseResults.success).to.be.true; + }); + + it(`should throw if deploying only ${TokenType.collateralVaultRebase}`, async () => { + config = { + arbitrum: { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + }; + let parseResults = WarpRouteDeployConfigSchema.safeParse(config); + expect(parseResults.success).to.be.false; + + config.ethereum = { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }; + parseResults = WarpRouteDeployConfigSchema.safeParse(config); + expect(parseResults.success).to.be.false; + }); + + it(`should derive the collateral chain name for ${TokenType.syntheticRebase}`, async () => { + config = { + arbitrum: { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + ethereum: { + type: TokenType.syntheticRebase, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + collateralChainName: '', + }, + optimism: { + type: TokenType.syntheticRebase, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + collateralChainName: '', + }, + }; + const parseResults = WarpRouteDeployConfigSchema.safeParse(config); + assert(parseResults.success, 'must be true'); + const warpConfig: WarpRouteDeployConfig = parseResults.data; + + assert( + warpConfig.optimism.type === TokenType.syntheticRebase, + 'must be syntheticRebase', + ); + expect(warpConfig.optimism.collateralChainName).to.equal('arbitrum'); + }); }); diff --git a/typescript/sdk/src/token/schemas.ts b/typescript/sdk/src/token/schemas.ts index 8ce070c1e..1ef4a770b 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -1,10 +1,16 @@ import { z } from 'zod'; +import { objMap } from '@hyperlane-xyz/utils'; + import { GasRouterConfigSchema } from '../router/schemas.js'; import { isCompliant } from '../utils/schemas.js'; import { TokenType } from './config.js'; +export const WarpRouteDeployConfigSchemaErrors = { + ONLY_SYNTHETIC_REBASE: `Config with ${TokenType.collateralVaultRebase} must be deployed with ${TokenType.syntheticRebase}`, + NO_SYNTHETIC_ONLY: `Config must include Native or Collateral OR all synthetics must define token metadata`, +}; export const TokenMetadataSchema = z.object({ name: z.string(), symbol: z.string(), @@ -18,6 +24,7 @@ export const CollateralConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([ TokenType.collateral, TokenType.collateralVault, + TokenType.collateralVaultRebase, TokenType.XERC20, TokenType.XERC20Lockbox, TokenType.collateralFiat, @@ -33,6 +40,18 @@ export const NativeConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([TokenType.native, TokenType.nativeScaled]), }); +export const CollateralRebaseConfigSchema = + TokenMetadataSchema.partial().extend({ + type: z.literal(TokenType.collateralVaultRebase), + }); + +export const SyntheticRebaseConfigSchema = TokenMetadataSchema.partial().extend( + { + type: z.literal(TokenType.syntheticRebase), + collateralChainName: z.string(), + }, +); + export const SyntheticConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([ TokenType.synthetic, @@ -50,6 +69,7 @@ export const TokenConfigSchema = z.discriminatedUnion('type', [ NativeConfigSchema, CollateralConfigSchema, SyntheticConfigSchema, + SyntheticRebaseConfigSchema, ]); export const TokenRouterConfigSchema = TokenConfigSchema.and( @@ -61,6 +81,10 @@ export type NativeConfig = z.infer; export type CollateralConfig = z.infer; export const isSyntheticConfig = isCompliant(SyntheticConfigSchema); +export const isSyntheticRebaseConfig = isCompliant(SyntheticRebaseConfigSchema); +export const isCollateralRebaseConfig = isCompliant( + CollateralRebaseConfigSchema, +); export const isCollateralConfig = isCompliant(CollateralConfigSchema); export const isNativeConfig = isCompliant(NativeConfigSchema); export const isTokenMetadata = isCompliant(TokenMetadataSchema); @@ -71,7 +95,49 @@ export const WarpRouteDeployConfigSchema = z const entries = Object.entries(configMap); return ( entries.some( - ([_, config]) => isCollateralConfig(config) || isNativeConfig(config), + ([_, config]) => + isCollateralConfig(config) || + isCollateralRebaseConfig(config) || + isNativeConfig(config), ) || entries.every(([_, config]) => isTokenMetadata(config)) ); - }, `Config must include Native or Collateral OR all synthetics must define token metadata`); + }, WarpRouteDeployConfigSchemaErrors.NO_SYNTHETIC_ONLY) + .transform((warpRouteDeployConfig, ctx) => { + const collateralRebaseEntry = Object.entries(warpRouteDeployConfig).find( + ([_, config]) => isCollateralRebaseConfig(config), + ); + if (!collateralRebaseEntry) return warpRouteDeployConfig; // Pass through for other token types + + if (isCollateralRebasePairedCorrectly(warpRouteDeployConfig)) { + const collateralChainName = collateralRebaseEntry[0]; + return objMap(warpRouteDeployConfig, (_, config) => { + if (config.type === TokenType.syntheticRebase) + config.collateralChainName = collateralChainName; + return config; + }) as Record; + } + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE, + }); + + return z.NEVER; // Causes schema validation to throw with above issue + }); + +function isCollateralRebasePairedCorrectly( + warpRouteDeployConfig: Record, +): boolean { + // Filter out all the non-collateral rebase configs to check if they are only synthetic rebase tokens + const otherConfigs = Object.entries(warpRouteDeployConfig).filter( + ([_, config]) => !isCollateralRebaseConfig(config), + ); + + if (otherConfigs.length === 0) return false; + + // The other configs MUST be synthetic rebase + const allOthersSynthetic: boolean = otherConfigs.every( + ([_, config], _index) => isSyntheticRebaseConfig(config), + ); + return allOthersSynthetic; +} diff --git a/yarn.lock b/yarn.lock index bae817923..910c021e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7827,6 +7827,7 @@ __metadata: "@hyperlane-xyz/sdk": "npm:5.5.0" "@hyperlane-xyz/utils": "npm:5.5.0" "@inquirer/prompts": "npm:^3.0.0" + "@types/chai-as-promised": "npm:^8" "@types/mocha": "npm:^10.0.1" "@types/node": "npm:^18.14.5" "@types/yargs": "npm:^17.0.24" @@ -7834,7 +7835,8 @@ __metadata: "@typescript-eslint/parser": "npm:^7.4.0" asn1.js: "npm:^5.4.1" bignumber.js: "npm:^9.1.1" - chai: "npm:4.5.0" + chai: "npm:^4.5.0" + chai-as-promised: "npm:^8.0.0" chalk: "npm:^5.3.0" eslint: "npm:^8.57.0" eslint-config-prettier: "npm:^9.1.0" @@ -13119,6 +13121,15 @@ __metadata: languageName: node linkType: hard +"@types/chai-as-promised@npm:^8": + version: 8.0.0 + resolution: "@types/chai-as-promised@npm:8.0.0" + dependencies: + "@types/chai": "npm:*" + checksum: f6db5698e4f28fd6e3914740810f356269b7f4e93a0650b38a9b01a1bae030593487c80bc57a0e69dd0bfb069a61d3dd285bfcfba6d1daf66ef3939577b68169 + languageName: node + linkType: hard + "@types/chai@npm:*, @types/chai@npm:^4.2.21": version: 4.3.1 resolution: "@types/chai@npm:4.3.1" @@ -16218,7 +16229,18 @@ __metadata: languageName: node linkType: hard -"chai@npm:4.5.0, chai@npm:^4.3.10, chai@npm:^4.3.7": +"chai-as-promised@npm:^8.0.0": + version: 8.0.0 + resolution: "chai-as-promised@npm:8.0.0" + dependencies: + check-error: "npm:^2.0.0" + peerDependencies: + chai: ">= 2.1.2 < 6" + checksum: 91d6a49caac7965440b8f8af421ebe6f060a3b5523599ae143816d08fc19d9a971ea2bc5401f82ce88d15d8bc7b64d356bf3e53542ace9e2f25cc454164d3247 + languageName: node + linkType: hard + +"chai@npm:4.5.0, chai@npm:^4.3.10, chai@npm:^4.3.7, chai@npm:^4.5.0": version: 4.5.0 resolution: "chai@npm:4.5.0" dependencies: @@ -16338,6 +16360,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.0.0": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: d785ed17b1d4a4796b6e75c765a9a290098cf52ff9728ce0756e8ffd4293d2e419dd30c67200aee34202463b474306913f2fcfaf1890641026d9fc6966fea27a + languageName: node + linkType: hard + "chokidar@npm:3.3.0": version: 3.3.0 resolution: "chokidar@npm:3.3.0" From 6295693f314c118e33f246c267a29e5aa2bda173 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Thu, 17 Oct 2024 10:36:39 +0100 Subject: [PATCH 04/26] chore: extend list of blocked addresses on relayer (#4696) ### Description ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- .../config/environments/mainnet3/agent.ts | 6 ++-- typescript/infra/src/config/agent/relayer.ts | 30 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 6e34d70cf..a470b1c87 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -418,7 +418,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: 'efd438f-20241016-101828', + tag: 'b1ff48b-20241016-183301', }, gasPaymentEnforcement: gasPaymentEnforcement, metricAppContexts, @@ -452,7 +452,7 @@ const releaseCandidate: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '5cb1787-20240924-192934', + tag: 'b1ff48b-20241016-183301', }, // We're temporarily (ab)using the RC relayer as a way to increase // message throughput. @@ -485,7 +485,7 @@ const neutron: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '5a0d68b-20240916-144115', + tag: 'b1ff48b-20241016-183301', }, gasPaymentEnforcement: [ { diff --git a/typescript/infra/src/config/agent/relayer.ts b/typescript/infra/src/config/agent/relayer.ts index aebcac245..cd7e4a46d 100644 --- a/typescript/infra/src/config/agent/relayer.ts +++ b/typescript/infra/src/config/agent/relayer.ts @@ -169,16 +169,26 @@ export class RelayerConfigHelper extends AgentConfigHelper { }), ); - return allSanctionedAddresses.flat().filter((address) => { - if (!isValidAddressEvm(address)) { - this.logger.debug( - { address }, - 'Invalid sanctioned address, throwing out', - ); - return false; - } - return true; - }); + const sanctionedEthereumAdresses = allSanctionedAddresses + .flat() + .filter((address) => { + if (!isValidAddressEvm(address)) { + this.logger.debug( + { address }, + 'Invalid sanctioned address, throwing out', + ); + return false; + } + return true; + }); + + const radiantExploiter = [ + '0xA0e768A68ba1BFffb9F4366dfC8D9195EE7217d1', + '0x0629b1048298AE9deff0F4100A31967Fb3f98962', + '0x97a05beCc2e7891D07F382457Cd5d57FD242e4e8', + ]; + + return [...sanctionedEthereumAdresses, ...radiantExploiter]; } // Returns whether the relayer requires AWS credentials From b3495b205c90fa4dbfc28b79ab492fbfb424386c Mon Sep 17 00:00:00 2001 From: Lee <6251863+ltyu@users.noreply.github.com> Date: Thu, 17 Oct 2024 06:47:57 -0400 Subject: [PATCH 05/26] chore: Update warp Ids for Renzo (#4695) ### Description Updates the warp Ids for latest renzo deployment to sei and taiko used by the checker --- .changeset/silver-dancers-rhyme.md | 5 +++++ .../infra/config/environments/mainnet3/warp/warpIds.ts | 2 +- typescript/infra/config/warp.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/silver-dancers-rhyme.md diff --git a/.changeset/silver-dancers-rhyme.md b/.changeset/silver-dancers-rhyme.md new file mode 100644 index 000000000..14ce7dd00 --- /dev/null +++ b/.changeset/silver-dancers-rhyme.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/infra': minor +--- + +Updates the warpIds for Renzo's latest deployment to Sei and Taiko to be used by the Checker diff --git a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts index 5e8052d42..3d9d08899 100644 --- a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts +++ b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts @@ -1,6 +1,6 @@ export enum WarpRouteIds { Ancient8EthereumUSDC = 'USDC/ancient8-ethereum', - ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismZircuitEZETH = 'EZETH/arbitrum-base-blast-bsc-ethereum-fraxtal-linea-mode-optimism-zircuit', + ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismSeiTaikoZircuitEZETH = 'EZETH/arbitrum-base-blast-bsc-ethereum-fraxtal-linea-mode-optimism-sei-taiko-zircuit', ArbitrumNeutronEclip = 'ECLIP/arbitrum-neutron', ArbitrumNeutronTIA = 'TIA/arbitrum-neutron', EclipseSolanaSOL = 'SOL/eclipsemainnet-solanamainnet', diff --git a/typescript/infra/config/warp.ts b/typescript/infra/config/warp.ts index 994c9fa9c..4b122718c 100644 --- a/typescript/infra/config/warp.ts +++ b/typescript/infra/config/warp.ts @@ -38,7 +38,7 @@ export const warpConfigGetterMap: Record< [WarpRouteIds.EthereumInevmUSDT]: getEthereumInevmUSDTWarpConfig, [WarpRouteIds.ArbitrumNeutronEclip]: getArbitrumNeutronEclipWarpConfig, [WarpRouteIds.ArbitrumNeutronTIA]: getArbitrumNeutronTiaWarpConfig, - [WarpRouteIds.ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismZircuitEZETH]: + [WarpRouteIds.ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismSeiTaikoZircuitEZETH]: getRenzoEZETHWarpConfig, [WarpRouteIds.InevmInjectiveINJ]: getInevmInjectiveINJWarpConfig, [WarpRouteIds.EthereumVictionETH]: getEthereumVictionETHWarpConfig, From 6dd47465964e61e1b308bfcf129ce121bc08ddfd Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Thu, 17 Oct 2024 11:48:04 +0100 Subject: [PATCH 06/26] chore: update key funder image (#4698) ### Description Updates the key funder to include the latest polygon tx overrides added by #4682 ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- typescript/infra/config/environments/mainnet3/funding.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/infra/config/environments/mainnet3/funding.ts b/typescript/infra/config/environments/mainnet3/funding.ts index 2dcd8d134..a9bd1ec0c 100644 --- a/typescript/infra/config/environments/mainnet3/funding.ts +++ b/typescript/infra/config/environments/mainnet3/funding.ts @@ -10,7 +10,7 @@ export const keyFunderConfig: KeyFunderConfig< > = { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '18c29c8-20241014-133714', + tag: '6295693-20241017-095058', }, // We're currently using the same deployer/key funder key as mainnet2. // To minimize nonce clobbering we offset the key funder cron From b4d26ddb7fc7fe9dad32fdbbf15a55bec1e3ed1c Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Thu, 17 Oct 2024 13:01:00 +0100 Subject: [PATCH 07/26] feat: add FASTUSD/ethereum-sei to checker (#4697) ### Description Adds recent fastUSD deploy to our checking infra ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- .../getEthereumSeiFastUSDWarpConfig.ts | 47 +++++++++++++++++++ .../environments/mainnet3/warp/warpIds.ts | 1 + typescript/infra/config/warp.ts | 12 +++-- typescript/infra/src/config/warp.ts | 4 ++ 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts new file mode 100644 index 000000000..5bd4938d2 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts @@ -0,0 +1,47 @@ +import { ethers } from 'ethers'; + +import { + ChainMap, + RouterConfig, + TokenRouterConfig, + TokenType, +} from '@hyperlane-xyz/sdk'; + +import { tokens } from '../../../../../src/config/warp.js'; + +// Elixir +const owner = '0x00000000F51340906F767C6999Fe512b1275955C'; + +export const getEthereumSeiFastUSDWarpConfig = async ( + routerConfig: ChainMap, +): Promise> => { + const sei: TokenRouterConfig = { + ...routerConfig.viction, + type: TokenType.XERC20, + name: 'fastUSD', + symbol: 'fastUSD', + decimals: 18, + token: tokens.sei.fastUSD, + interchainSecurityModule: ethers.constants.AddressZero, + owner, + ownerOverrides: { + proxyAdmin: owner, + }, + }; + + const ethereum: TokenRouterConfig = { + ...routerConfig.ethereum, + type: TokenType.collateral, + token: tokens.ethereum.deUSD, + owner, + interchainSecurityModule: ethers.constants.AddressZero, + ownerOverrides: { + proxyAdmin: owner, + }, + }; + + return { + sei, + ethereum, + }; +}; diff --git a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts index 3d9d08899..7a75bcd2c 100644 --- a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts +++ b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts @@ -9,6 +9,7 @@ export enum WarpRouteIds { EthereumInevmUSDT = 'USDT/ethereum-inevm', EthereumEclipseTETH = 'tETH/eclipsemainnet-ethereum', EthereumEclipseUSDC = 'USDC/eclipsemainnet-ethereum-solanamainnet', + EthereumSeiFastUSD = 'FASTUSD/ethereum-sei', EthereumVictionETH = 'ETH/ethereum-viction', EthereumVictionUSDC = 'USDC/ethereum-viction', EthereumVictionUSDT = 'USDT/ethereum-viction', diff --git a/typescript/infra/config/warp.ts b/typescript/infra/config/warp.ts index 4b122718c..7cfb624f3 100644 --- a/typescript/infra/config/warp.ts +++ b/typescript/infra/config/warp.ts @@ -15,6 +15,7 @@ import { getEthereumEclipseTETHWarpConfig } from './environments/mainnet3/warp/c import { getEthereumEclipseUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumEclipseUSDCWarpConfig.js'; import { getEthereumInevmUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumInevmUSDCWarpConfig.js'; import { getEthereumInevmUSDTWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumInevmUSDTWarpConfig.js'; +import { getEthereumSeiFastUSDWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.js'; import { getEthereumVictionETHWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.js'; import { getEthereumVictionUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.js'; import { getEthereumVictionUSDTWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.js'; @@ -41,12 +42,13 @@ export const warpConfigGetterMap: Record< [WarpRouteIds.ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismSeiTaikoZircuitEZETH]: getRenzoEZETHWarpConfig, [WarpRouteIds.InevmInjectiveINJ]: getInevmInjectiveINJWarpConfig, + [WarpRouteIds.EthereumEclipseTETH]: getEthereumEclipseTETHWarpConfig, + [WarpRouteIds.EthereumEclipseUSDC]: getEthereumEclipseUSDCWarpConfig, + [WarpRouteIds.EthereumSeiFastUSD]: getEthereumSeiFastUSDWarpConfig, [WarpRouteIds.EthereumVictionETH]: getEthereumVictionETHWarpConfig, [WarpRouteIds.EthereumVictionUSDC]: getEthereumVictionUSDCWarpConfig, [WarpRouteIds.EthereumVictionUSDT]: getEthereumVictionUSDTWarpConfig, [WarpRouteIds.MantapacificNeutronTIA]: getMantapacificNeutronTiaWarpConfig, - [WarpRouteIds.EthereumEclipseTETH]: getEthereumEclipseTETHWarpConfig, - [WarpRouteIds.EthereumEclipseUSDC]: getEthereumEclipseUSDCWarpConfig, }; export async function getWarpConfig( @@ -59,7 +61,11 @@ export async function getWarpConfig( const warpConfigGetter = warpConfigGetterMap[warpRouteId]; if (!warpConfigGetter) { - throw new Error(`Unknown warp route: ${warpRouteId}`); + throw new Error( + `Unknown warp route: ${warpRouteId}, must be one of: ${Object.keys( + warpConfigGetterMap, + ).join(', ')}`, + ); } if (warpConfigGetter.length === 1) { diff --git a/typescript/infra/src/config/warp.ts b/typescript/infra/src/config/warp.ts index cf9d125d8..25bd40818 100644 --- a/typescript/infra/src/config/warp.ts +++ b/typescript/infra/src/config/warp.ts @@ -6,5 +6,9 @@ export const tokens: ChainMap> = { ethereum: { USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', USDT: '0xdac17f958d2ee523a2206206994597c13d831ec7', + deUSD: '0x15700B564Ca08D9439C58cA5053166E8317aa138', + }, + sei: { + fastUSD: '0x37a4dD9CED2b19Cfe8FAC251cd727b5787E45269', }, }; From 1606f85df30e5f5df453584911058751c2698cc5 Mon Sep 17 00:00:00 2001 From: Mohammed Hussan Date: Thu, 17 Oct 2024 07:54:52 -0500 Subject: [PATCH 08/26] feat(warpChecker): Support Renzo PZETH route (#4628) ### Description - support Renzo PZETH warp route in checker ### Drive-by changes - introduce `CheckerWarpRouteIds` enum for warp routes that are supported in the checker, eclipse and sol routes are not currently supported ### Testing Manual --- .../infra/config/environments/mainnet3/warp/warpIds.ts | 1 + typescript/infra/config/warp.ts | 6 ++---- typescript/infra/scripts/check/check-warp-deploy.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts index 7a75bcd2c..9ff51677e 100644 --- a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts +++ b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts @@ -13,6 +13,7 @@ export enum WarpRouteIds { EthereumVictionETH = 'ETH/ethereum-viction', EthereumVictionUSDC = 'USDC/ethereum-viction', EthereumVictionUSDT = 'USDT/ethereum-viction', + EthereumZircuitPZETH = 'PZETH/ethereum-zircuit', InevmInjectiveINJ = 'INJ/inevm-injective', MantapacificNeutronTIA = 'TIA/mantapacific-neutron', } diff --git a/typescript/infra/config/warp.ts b/typescript/infra/config/warp.ts index 7cfb624f3..447183e8c 100644 --- a/typescript/infra/config/warp.ts +++ b/typescript/infra/config/warp.ts @@ -11,8 +11,6 @@ import { EnvironmentConfig } from '../src/config/environment.js'; import { getAncient8EthereumUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getAncient8EthereumUSDCWarpConfig.js'; import { getArbitrumNeutronEclipWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumNeutronEclipWarpConfig.js'; import { getArbitrumNeutronTiaWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumNeutronTiaWarpConfig.js'; -import { getEthereumEclipseTETHWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumEclipseTETHWarpConfig.js'; -import { getEthereumEclipseUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumEclipseUSDCWarpConfig.js'; import { getEthereumInevmUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumInevmUSDCWarpConfig.js'; import { getEthereumInevmUSDTWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumInevmUSDTWarpConfig.js'; import { getEthereumSeiFastUSDWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.js'; @@ -22,6 +20,7 @@ import { getEthereumVictionUSDTWarpConfig } from './environments/mainnet3/warp/c import { getInevmInjectiveINJWarpConfig } from './environments/mainnet3/warp/configGetters/getInevmInjectiveINJWarpConfig.js'; import { getMantapacificNeutronTiaWarpConfig } from './environments/mainnet3/warp/configGetters/getMantapacificNeutronTiaWarpConfig.js'; import { getRenzoEZETHWarpConfig } from './environments/mainnet3/warp/configGetters/getRenzoEZETHWarpConfig.js'; +import { getRenzoPZETHWarpConfig } from './environments/mainnet3/warp/configGetters/getRenzoPZETHWarpConfig.js'; import { WarpRouteIds } from './environments/mainnet3/warp/warpIds.js'; type WarpConfigGetterWithConfig = ( @@ -42,12 +41,11 @@ export const warpConfigGetterMap: Record< [WarpRouteIds.ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismSeiTaikoZircuitEZETH]: getRenzoEZETHWarpConfig, [WarpRouteIds.InevmInjectiveINJ]: getInevmInjectiveINJWarpConfig, - [WarpRouteIds.EthereumEclipseTETH]: getEthereumEclipseTETHWarpConfig, - [WarpRouteIds.EthereumEclipseUSDC]: getEthereumEclipseUSDCWarpConfig, [WarpRouteIds.EthereumSeiFastUSD]: getEthereumSeiFastUSDWarpConfig, [WarpRouteIds.EthereumVictionETH]: getEthereumVictionETHWarpConfig, [WarpRouteIds.EthereumVictionUSDC]: getEthereumVictionUSDCWarpConfig, [WarpRouteIds.EthereumVictionUSDT]: getEthereumVictionUSDTWarpConfig, + [WarpRouteIds.EthereumZircuitPZETH]: getRenzoPZETHWarpConfig, [WarpRouteIds.MantapacificNeutronTIA]: getMantapacificNeutronTiaWarpConfig, }; diff --git a/typescript/infra/scripts/check/check-warp-deploy.ts b/typescript/infra/scripts/check/check-warp-deploy.ts index 66334b8cf..aa51c7016 100644 --- a/typescript/infra/scripts/check/check-warp-deploy.ts +++ b/typescript/infra/scripts/check/check-warp-deploy.ts @@ -1,7 +1,7 @@ import chalk from 'chalk'; import { Gauge, Registry } from 'prom-client'; -import { WarpRouteIds } from '../../config/environments/mainnet3/warp/warpIds.js'; +import { warpConfigGetterMap } from '../../config/warp.js'; import { submitMetrics } from '../../src/utils/metrics.js'; import { Modules } from '../agent-utils.js'; @@ -25,7 +25,7 @@ async function main() { const failedWarpRoutesChecks: string[] = []; // TODO: consider retrying this if check throws an error - for (const warpRouteId of Object.values(WarpRouteIds)) { + for (const warpRouteId of Object.keys(warpConfigGetterMap)) { console.log(`\nChecking warp route ${warpRouteId}...`); const warpModule = Modules.WARP; From dbb7e954f545b3dd557b45fc17a5f9a9b9f767db Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Thu, 17 Oct 2024 16:07:09 +0100 Subject: [PATCH 09/26] fix: use correct contract for proxyAdmin ownership violation (#4700) ### Description Quick change I encountered when trying to change proxy admin owners -- the contract passed in was the app router, not the proxy admin ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- typescript/sdk/src/deploy/HyperlaneAppChecker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts index 871019a17..9a9bf4bcc 100644 --- a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts +++ b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts @@ -139,7 +139,7 @@ export abstract class HyperlaneAppChecker< type: ViolationType.Owner, actual: actualProxyAdminOwner, expected: expectedOwner, - contract, + contract: actualProxyAdminContract, }; this.addViolation(violation); } From 3f48f5a8ed5ed872ddbb5a592032a54352638f66 Mon Sep 17 00:00:00 2001 From: Paul Balaji <10051819+paulbalaji@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:36:05 +0100 Subject: [PATCH 10/26] chore: add scroll gasPrice override (#4701) ### Description chore: add back the scroll gasPrice override relayer's unaffected because we override that to 0.8 gwei ### Drive-by changes update agent config ### Related issues key funder :'( ### Backward compatibility yes ### Testing manual? --------- Signed-off-by: pbio <10051819+paulbalaji@users.noreply.github.com> --- rust/main/config/mainnet_config.json | 5 ++++- typescript/infra/config/environments/mainnet3/chains.ts | 8 ++++++++ typescript/infra/config/environments/mainnet3/funding.ts | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/rust/main/config/mainnet_config.json b/rust/main/config/mainnet_config.json index c460fe308..2667590fe 100644 --- a/rust/main/config/mainnet_config.json +++ b/rust/main/config/mainnet_config.json @@ -2706,7 +2706,10 @@ "validatorAnnounce": "0xd83A4F747fE80Ed98839e05079B1B7Fe037b1638", "staticMerkleRootWeightedMultisigIsmFactory": "0xcb0D04010584AA5244b5826c990eeA4c16BeAC8C", "staticMessageIdWeightedMultisigIsmFactory": "0x609707355a53d2aAb6366f48E2b607C599D26B29", - "technicalStack": "other" + "technicalStack": "other", + "transactionOverrides": { + "gasPrice": 200000000 + } }, "sei": { "aggregationHook": "0x40514BD46C57455933Be8BAedE96C4F0Ba3507D6", diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index e7ff32d46..e4d7f774f 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -31,6 +31,14 @@ export const chainMetadataOverrides: ChainMap> = { gasPrice: 1 * 10 ** 9, // 1 gwei }, }, + scroll: { + transactionOverrides: { + // Scroll doesn't use EIP 1559 and the gas price that's returned is sometimes + // too low for the transaction to be included in a reasonable amount of time - + // this often leads to transaction underpriced issues. + gasPrice: 2 * 10 ** 8, // 0.2 gwei + }, + }, sei: { // Sei's `eth_feeHistory` is not to spec and incompatible with ethers-rs, // so we force legacy transactions by setting a gas price. diff --git a/typescript/infra/config/environments/mainnet3/funding.ts b/typescript/infra/config/environments/mainnet3/funding.ts index a9bd1ec0c..dafee524d 100644 --- a/typescript/infra/config/environments/mainnet3/funding.ts +++ b/typescript/infra/config/environments/mainnet3/funding.ts @@ -10,7 +10,7 @@ export const keyFunderConfig: KeyFunderConfig< > = { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '6295693-20241017-095058', + tag: '436988a-20241017-151047', }, // We're currently using the same deployer/key funder key as mainnet2. // To minimize nonce clobbering we offset the key funder cron From 01e7070ebe4f53a6298296eaf193bbdf3bd65da6 Mon Sep 17 00:00:00 2001 From: xeno097 Date: Thu, 17 Oct 2024 18:30:05 -0400 Subject: [PATCH 11/26] feat: better chain selection concept (#4596) ### Description This PR implements an updated view of the multi-chain selection step that now allows searching for chains in the current list #### Before: ![image](https://github.com/user-attachments/assets/64876be9-16f6-4c23-8562-637776d1db0a) ![image](https://github.com/user-attachments/assets/165c46c5-e94a-48b6-aa7c-38a68b20eed7) #### After: ![image](https://github.com/user-attachments/assets/1c91c34f-c7aa-43df-8de6-7e7322c1ba70) ![image](https://github.com/user-attachments/assets/adfac628-a9c2-4c28-85a2-853dea1da551) ![image](https://github.com/user-attachments/assets/809fef22-9a2a-4220-8192-9108ae1e093e) ### Drive-by changes - Updated the `runMultiChainSelectionStep` function to take as param an object instead of a list of params because the list was growing larger ### Related issues - Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4513 ### Backward compatibility - Yes ### Testing - Manual - Manual testing has also been conducted on Windows to see if https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4508 was solved. In this case, the following were discovered: - The chain selection is unusable on `gitbash`. - The chain selection works perfectly fine using `powershell` and `cmd`. I assume the issue is linked to how `gitbash` handles inputs or simulates a UNIX environment on Windows. CLI users on windows should use either one of these options --- .changeset/tricky-mangos-sin.md | 5 + typescript/cli/package.json | 1 + typescript/cli/src/config/hooks.ts | 10 +- typescript/cli/src/config/ism.ts | 20 +- typescript/cli/src/config/multisig.ts | 4 +- typescript/cli/src/config/warp.ts | 11 +- typescript/cli/src/deploy/agent.ts | 10 +- typescript/cli/src/utils/chains.ts | 108 +++++- typescript/cli/src/utils/input.ts | 509 +++++++++++++++++++++++++- yarn.lock | 17 + 10 files changed, 649 insertions(+), 46 deletions(-) create mode 100644 .changeset/tricky-mangos-sin.md diff --git a/.changeset/tricky-mangos-sin.md b/.changeset/tricky-mangos-sin.md new file mode 100644 index 000000000..31ea11b57 --- /dev/null +++ b/.changeset/tricky-mangos-sin.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +updates the multi chain selection prompt by adding search functionality and an optional confirmation prompt for the current selection diff --git a/typescript/cli/package.json b/typescript/cli/package.json index eb1403de0..ee91e5fc8 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -9,6 +9,7 @@ "@hyperlane-xyz/sdk": "5.5.0", "@hyperlane-xyz/utils": "5.5.0", "@inquirer/prompts": "^3.0.0", + "ansi-escapes": "^7.0.0", "asn1.js": "^5.4.1", "bignumber.js": "^9.1.1", "chalk": "^5.3.0", diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index 30294d19d..a075d6655 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -265,11 +265,11 @@ export const createRoutingConfig = callWithConfigCreationLogs( message: 'Enter owner address for routing Hook', }); const ownerAddress = owner; - const chains = await runMultiChainSelectionStep( - context.chainMetadata, - 'Select chains for routing Hook', - 1, - ); + const chains = await runMultiChainSelectionStep({ + chainMetadata: context.chainMetadata, + message: 'Select chains for routing Hook', + requireNumber: 1, + }); const domainsMap: ChainMap = {}; for (const chain of chains) { diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts index c975bb895..e40a00c26 100644 --- a/typescript/cli/src/config/ism.ts +++ b/typescript/cli/src/config/ism.ts @@ -226,11 +226,11 @@ export const createRoutingConfig = callWithConfigCreationLogs( message: 'Enter owner address for routing ISM', }); const ownerAddress = owner; - const chains = await runMultiChainSelectionStep( - context.chainMetadata, - 'Select chains to configure routing ISM for', - 1, - ); + const chains = await runMultiChainSelectionStep({ + chainMetadata: context.chainMetadata, + message: 'Select chains to configure routing ISM for', + requireNumber: 1, + }); const domainsMap: ChainMap = {}; for (const chain of chains) { @@ -249,11 +249,11 @@ export const createRoutingConfig = callWithConfigCreationLogs( export const createFallbackRoutingConfig = callWithConfigCreationLogs( async (context: CommandContext): Promise => { - const chains = await runMultiChainSelectionStep( - context.chainMetadata, - 'Select chains to configure fallback routing ISM for', - 1, - ); + const chains = await runMultiChainSelectionStep({ + chainMetadata: context.chainMetadata, + message: 'Select chains to configure fallback routing ISM for', + requireNumber: 1, + }); const domainsMap: ChainMap = {}; for (const chain of chains) { diff --git a/typescript/cli/src/config/multisig.ts b/typescript/cli/src/config/multisig.ts index bb2e0ebbf..28648b271 100644 --- a/typescript/cli/src/config/multisig.ts +++ b/typescript/cli/src/config/multisig.ts @@ -72,7 +72,9 @@ export async function createMultisigConfig({ log( 'Select your own chain below to run your own validators. If you want to reuse existing Hyperlane validators instead of running your own, do not select additional mainnet or testnet chains.', ); - const chains = await runMultiChainSelectionStep(context.chainMetadata); + const chains = await runMultiChainSelectionStep({ + chainMetadata: context.chainMetadata, + }); const chainAddresses = await context.registry.getAddresses(); const result: MultisigConfigMap = {}; diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 29246cd33..dd3a23713 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -125,11 +125,12 @@ export async function createWarpRouteDeployConfig({ 'signer', ); - const warpChains = await runMultiChainSelectionStep( - context.chainMetadata, - 'Select chains to connect', - 1, - ); + const warpChains = await runMultiChainSelectionStep({ + chainMetadata: context.chainMetadata, + message: 'Select chains to connect', + requireNumber: 1, + requiresConfirmation: true, + }); const result: WarpRouteDeployConfig = {}; let typeChoices = TYPE_CHOICES; diff --git a/typescript/cli/src/deploy/agent.ts b/typescript/cli/src/deploy/agent.ts index 5b93e10c4..ca490fc5f 100644 --- a/typescript/cli/src/deploy/agent.ts +++ b/typescript/cli/src/deploy/agent.ts @@ -28,11 +28,11 @@ export async function runKurtosisAgentDeploy({ ); } if (!relayChains) { - const selectedRelayChains = await runMultiChainSelectionStep( - context.chainMetadata, - 'Select chains to relay between', - 2, - ); + const selectedRelayChains = await runMultiChainSelectionStep({ + chainMetadata: context.chainMetadata, + message: 'Select chains to relay between', + requireNumber: 2, + }); relayChains = selectedRelayChains.join(','); } diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts index fe226b1b8..09975aca3 100644 --- a/typescript/cli/src/utils/chains.ts +++ b/typescript/cli/src/utils/chains.ts @@ -1,13 +1,14 @@ -import { Separator, checkbox } from '@inquirer/prompts'; +import { Separator, confirm } from '@inquirer/prompts'; import select from '@inquirer/select'; import chalk from 'chalk'; import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk'; import { toTitleCase } from '@hyperlane-xyz/utils'; -import { log, logRed, logTip } from '../logger.js'; +import { log } from '../logger.js'; import { calculatePageSize } from './cli-options.js'; +import { SearchableCheckboxChoice, searchableCheckBox } from './input.js'; // A special value marker to indicate user selected // a new chain in the list @@ -18,37 +19,101 @@ export async function runSingleChainSelectionStep( message = 'Select chain', ) { const networkType = await selectNetworkType(); - const choices = getChainChoices(chainMetadata, networkType); + const { choices, networkTypeSeparator } = getChainChoices( + chainMetadata, + networkType, + ); const chain = (await select({ message, - choices, + choices: [networkTypeSeparator, ...choices], pageSize: calculatePageSize(2), })) as string; handleNewChain([chain]); return chain; } -export async function runMultiChainSelectionStep( - chainMetadata: ChainMap, +type RunMultiChainSelectionStepOptions = { + /** + * The metadata of the chains that will be displayed to the user + */ + chainMetadata: ChainMap; + + /** + * The message to display to the user + * + * @default 'Select chains' + */ + message?: string; + + /** + * The minimum number of chains that must be selected + * + * @default 0 + */ + requireNumber?: number; + + /** + * Whether to ask for confirmation after the selection + * + * @default false + */ + requiresConfirmation?: boolean; +}; + +export async function runMultiChainSelectionStep({ + chainMetadata, message = 'Select chains', requireNumber = 0, -) { + requiresConfirmation = false, +}: RunMultiChainSelectionStepOptions) { const networkType = await selectNetworkType(); - const choices = getChainChoices(chainMetadata, networkType); + const { choices, networkTypeSeparator } = getChainChoices( + chainMetadata, + networkType, + ); + + let currentChoiceSelection = new Set(); while (true) { - logTip( - `Use SPACE key to select at least ${requireNumber} chains, then press ENTER`, - ); - const chains = (await checkbox({ + const chains = await searchableCheckBox({ message, - choices, + selectableOptionsSeparator: networkTypeSeparator, + choices: choices.map((choice) => + currentChoiceSelection.has(choice.name) + ? { ...choice, checked: true } + : choice, + ), + instructions: `Use TAB key to select at least ${requireNumber} chains, then press ENTER to proceed. Type to search for a specific chain.`, + theme: { + style: { + // The leading space is needed because the help tip will be tightly close to the message header + helpTip: (text: string) => ` ${chalk.bgYellow(text)}`, + }, + helpMode: 'always', + }, pageSize: calculatePageSize(2), - })) as string[]; + validate: (answer): string | boolean => { + if (answer.length < requireNumber) { + return `Please select at least ${requireNumber} chains`; + } + + return true; + }, + }); + handleNewChain(chains); - if (chains?.length < requireNumber) { - logRed(`Please select at least ${requireNumber} chains`); + + const confirmed = requiresConfirmation + ? await confirm({ + message: `Is this chain selection correct?: ${chalk.cyan( + chains.join(', '), + )}`, + }) + : true; + if (!confirmed) { + currentChoiceSelection = new Set(chains); continue; } + return chains; } } @@ -75,12 +140,17 @@ function getChainChoices( const filteredChains = chains.filter((c) => networkType === 'mainnet' ? !c.isTestnet : !!c.isTestnet, ); - const choices: Parameters['0']['choices'] = [ + const choices: SearchableCheckboxChoice[] = [ { name: '(New custom chain)', value: NEW_CHAIN_MARKER }, - new Separator(`--${toTitleCase(networkType)} Chains--`), ...chainsToChoices(filteredChains), ]; - return choices; + + return { + choices, + networkTypeSeparator: new Separator( + `--${toTitleCase(networkType)} Chains--`, + ), + }; } function handleNewChain(chainNames: string[]) { diff --git a/typescript/cli/src/utils/input.ts b/typescript/cli/src/utils/input.ts index 0f8c9ef66..4b54c4f3e 100644 --- a/typescript/cli/src/utils/input.ts +++ b/typescript/cli/src/utils/input.ts @@ -1,4 +1,22 @@ -import { confirm, input } from '@inquirer/prompts'; +import { + Separator, + type Theme, + createPrompt, + isEnterKey, + makeTheme, + useEffect, + useKeypress, + useMemo, + usePagination, + usePrefix, + useRef, + useState, +} from '@inquirer/core'; +import figures from '@inquirer/figures'; +import { KeypressEvent, confirm, input } from '@inquirer/prompts'; +import type { PartialDeep } from '@inquirer/type'; +import ansiEscapes from 'ansi-escapes'; +import chalk from 'chalk'; import { logGray } from '../logger.js'; @@ -53,3 +71,492 @@ export async function inputWithInfo({ } while (answer === INFO_COMMAND); return answer; } + +/** + * Searchable checkbox code + * + * Note that the code below hab been implemented by taking inspiration from + * the @inquirer/prompt package search and checkbox prompts + * + * - https://github.com/SBoudrias/Inquirer.js/blob/main/packages/search/src/index.mts + * - https://github.com/SBoudrias/Inquirer.js/blob/main/packages/checkbox/src/index.mts + */ + +type Status = 'loading' | 'idle' | 'done'; + +type SearchableCheckboxTheme = { + icon: { + checked: string; + unchecked: string; + cursor: string; + }; + style: { + disabledChoice: (text: string) => string; + renderSelectedChoices: ( + selectedChoices: ReadonlyArray>, + allChoices: ReadonlyArray | Separator>, + ) => string; + description: (text: string) => string; + helpTip: (text: string) => string; + }; + helpMode: 'always' | 'never' | 'auto'; +}; + +const checkboxTheme: SearchableCheckboxTheme = { + icon: { + checked: chalk.green(figures.circleFilled), + unchecked: figures.circle, + cursor: figures.pointer, + }, + style: { + disabledChoice: (text: string) => chalk.dim(`- ${text}`), + renderSelectedChoices: (selectedChoices) => + selectedChoices.map((choice) => choice.short).join(', '), + description: (text: string) => chalk.cyan(text), + helpTip: (text) => ` ${text}`, + }, + helpMode: 'always', +}; + +export type SearchableCheckboxChoice = { + value: Value; + name?: string; + description?: string; + short?: string; + disabled?: boolean | string; + checked?: boolean; +}; + +type NormalizedChoice = Required< + Omit, 'description'> +> & { + description?: string; +}; + +type SearchableCheckboxConfig = { + message: string; + prefix?: string; + pageSize?: number; + instructions?: string; + choices: ReadonlyArray>; + loop?: boolean; + required?: boolean; + selectableOptionsSeparator?: Separator; + validate?: ( + choices: ReadonlyArray>, + ) => boolean | string | Promise; + theme?: PartialDeep>; +}; + +type Item = NormalizedChoice | Separator; + +type SearchableCheckboxState = { + options: Item[]; + currentOptionState: Record>; +}; + +function isSelectable( + item: Item, +): item is NormalizedChoice { + return !Separator.isSeparator(item) && !item.disabled; +} + +function isChecked(item: Item): item is NormalizedChoice { + return isSelectable(item) && Boolean(item.checked); +} + +function toggle(item: Item): Item { + return isSelectable(item) ? { ...item, checked: !item.checked } : item; +} + +function normalizeChoices( + choices: ReadonlyArray>, +): NormalizedChoice[] { + return choices.map((choice) => { + const name = choice.name ?? String(choice.value); + return { + value: choice.value, + name, + short: choice.short ?? name, + description: choice.description, + disabled: choice.disabled ?? false, + checked: choice.checked ?? false, + }; + }); +} + +function sortNormalizedItems( + a: NormalizedChoice, + b: NormalizedChoice, +): number { + return a.name.localeCompare(b.name); +} + +function organizeItems( + items: Array>, + selectableOptionsSeparator?: Separator, +): Array | Separator> { + const orderedItems = []; + + const checkedItems = items.filter( + (item) => !Separator.isSeparator(item) && item.checked, + ) as NormalizedChoice[]; + + if (checkedItems.length !== 0) { + orderedItems.push(new Separator('--Selected Options--')); + + orderedItems.push(...checkedItems.sort(sortNormalizedItems)); + } + + orderedItems.push( + selectableOptionsSeparator ?? new Separator('--Available Options--'), + ); + + const nonCheckedItems = items.filter( + (item) => !Separator.isSeparator(item) && !item.checked, + ) as NormalizedChoice[]; + + orderedItems.push(...nonCheckedItems.sort(sortNormalizedItems)); + + if (orderedItems.length === 1) { + return []; + } + + return orderedItems; +} + +interface BuildViewOptions { + theme: Readonly>; + pageSize: number; + firstRender: { current: boolean }; + page: string; + currentOptions: ReadonlyArray>; + prefix: string; + message: string; + errorMsg?: string; + status: Status; + searchTerm: string; + description?: string; + instructions?: string; +} + +interface GetErrorMessageOptions + extends Pick< + BuildViewOptions, + 'theme' | 'errorMsg' | 'status' | 'searchTerm' + > { + currentItemCount: number; +} + +function getErrorMessage({ + theme, + errorMsg, + currentItemCount, + status, + searchTerm, +}: GetErrorMessageOptions): string { + if (errorMsg) { + return `${theme.style.error(errorMsg)}`; + } else if (currentItemCount === 0 && searchTerm !== '' && status === 'idle') { + return theme.style.error('No results found'); + } + + return ''; +} + +interface GetHelpTipsOptions + extends Pick< + BuildViewOptions, + 'theme' | 'pageSize' | 'firstRender' | 'instructions' + > { + currentItemCount: number; +} + +function getHelpTips({ + theme, + instructions, + currentItemCount, + pageSize, + firstRender, +}: GetHelpTipsOptions): { helpTipTop: string; helpTipBottom: string } { + let helpTipTop = ''; + let helpTipBottom = ''; + const defaultTopHelpTip = + instructions ?? + `(Press ${theme.style.key('tab')} to select, and ${theme.style.key( + 'enter', + )} to proceed`; + const defaultBottomHelpTip = `\n${theme.style.help( + '(Use arrow keys to reveal more choices)', + )}`; + + if (theme.helpMode === 'always') { + helpTipTop = theme.style.helpTip(defaultTopHelpTip); + helpTipBottom = currentItemCount > pageSize ? defaultBottomHelpTip : ''; + firstRender.current = false; + } else if (theme.helpMode === 'auto' && firstRender.current) { + helpTipTop = theme.style.helpTip(defaultTopHelpTip); + helpTipBottom = currentItemCount > pageSize ? defaultBottomHelpTip : ''; + firstRender.current = false; + } + + return { helpTipBottom, helpTipTop }; +} + +function formatRenderedItem( + item: Readonly>, + isActive: boolean, + theme: Readonly>, +): string { + if (Separator.isSeparator(item)) { + return ` ${item.separator}`; + } + + if (item.disabled) { + const disabledLabel = + typeof item.disabled === 'string' ? item.disabled : '(disabled)'; + return theme.style.disabledChoice(`${item.name} ${disabledLabel}`); + } + + const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked; + const color = isActive ? theme.style.highlight : (x: string) => x; + const cursor = isActive ? theme.icon.cursor : ' '; + return color(`${cursor}${checkbox} ${item.name}`); +} + +function getListBounds(items: ReadonlyArray>): { + first: number; + last: number; +} { + const first = items.findIndex(isSelectable); + // findLastIndex replacement as the project must support older ES versions + let last = -1; + for (let i = items.length; i >= 0; --i) { + if (items[i] && isSelectable(items[i])) { + last = i; + break; + } + } + + return { first, last }; +} + +function buildView({ + page, + prefix, + theme, + status, + message, + errorMsg, + pageSize, + firstRender, + searchTerm, + description, + instructions, + currentOptions, +}: BuildViewOptions): string { + message = theme.style.message(message); + if (status === 'done') { + const selection = currentOptions.filter(isChecked); + const answer = theme.style.answer( + theme.style.renderSelectedChoices(selection, currentOptions), + ); + + return `${prefix} ${message} ${answer}`; + } + + const currentItemCount = currentOptions.length; + const { helpTipBottom, helpTipTop } = getHelpTips({ + theme, + instructions, + currentItemCount, + pageSize, + firstRender, + }); + + const choiceDescription = description + ? `\n${theme.style.description(description)}` + : ``; + + const error = getErrorMessage({ + theme, + errorMsg, + currentItemCount, + status, + searchTerm, + }); + + return `${prefix} ${message}${helpTipTop} ${searchTerm}\n${page}${helpTipBottom}${choiceDescription}${error}${ansiEscapes.cursorHide}`; +} + +// the isUpKey function from the inquirer package is not used +// because it detects k and p as custom keybindings that cause +// the option selection to go up instead of writing the letters +// in the search string +function isUpKey(key: KeypressEvent): boolean { + return key.name === 'up'; +} + +// the isDownKey function from the inquirer package is not used +// because it detects j and n as custom keybindings that cause +// the option selection to go down instead of writing the letters +// in the search string +function isDownKey(key: KeypressEvent): boolean { + return key.name === 'down'; +} + +export const searchableCheckBox = createPrompt( + ( + config: SearchableCheckboxConfig, + done: (value: Array) => void, + ) => { + const { + instructions, + pageSize = 7, + loop = true, + required, + validate = () => true, + selectableOptionsSeparator, + } = config; + const theme = makeTheme( + checkboxTheme, + config.theme, + ); + const firstRender = useRef(true); + const [status, setStatus] = useState('idle'); + const prefix = usePrefix({ theme }); + const [searchTerm, setSearchTerm] = useState(''); + const [errorMsg, setError] = useState(); + + const normalizedChoices = normalizeChoices(config.choices); + const [optionState, setOptionState] = useState< + SearchableCheckboxState + >({ + options: normalizedChoices, + currentOptionState: Object.fromEntries( + normalizedChoices.map((item) => [item.name, item]), + ), + }); + + const bounds = useMemo( + () => getListBounds(optionState.options), + [optionState.options], + ); + + const [active, setActive] = useState(bounds.first); + + useEffect(() => { + let filteredItems; + if (!searchTerm) { + filteredItems = Object.values(optionState.currentOptionState); + } else { + filteredItems = Object.values(optionState.currentOptionState).filter( + (item) => + Separator.isSeparator(item) || + item.name.includes(searchTerm) || + item.checked, + ); + } + + setActive(0); + setError(undefined); + setOptionState({ + currentOptionState: optionState.currentOptionState, + options: organizeItems(filteredItems, selectableOptionsSeparator), + }); + }, [searchTerm]); + + useKeypress(async (key, rl) => { + if (isEnterKey(key)) { + const selection = optionState.options.filter(isChecked); + const isValid = await validate(selection); + if (required && !optionState.options.some(isChecked)) { + setError('At least one choice must be selected'); + } else if (isValid === true) { + setStatus('done'); + done(selection.map((choice) => choice.value)); + } else { + setError(isValid || 'You must select a valid value'); + setSearchTerm(''); + } + } else if (isUpKey(key) || isDownKey(key)) { + if ( + loop || + (isUpKey(key) && active !== bounds.first) || + (isDownKey(key) && active !== bounds.last) + ) { + const offset = isUpKey(key) ? -1 : 1; + let next = active; + do { + next = + (next + offset + optionState.options.length) % + optionState.options.length; + } while ( + optionState.options[next] && + !isSelectable(optionState.options[next]) + ); + setActive(next); + } + } else if (key.name === 'tab' && optionState.options.length > 0) { + // Avoid the message header to be printed again in the console + rl.clearLine(0); + + const currentElement = optionState.options[active]; + if ( + currentElement && + !Separator.isSeparator(currentElement) && + optionState.currentOptionState[currentElement.name] + ) { + const updatedDataMap: Record> = { + ...optionState.currentOptionState, + [currentElement.name]: toggle( + optionState.currentOptionState[currentElement.name], + ) as NormalizedChoice, + }; + + setError(undefined); + setOptionState({ + options: organizeItems( + Object.values(updatedDataMap), + selectableOptionsSeparator, + ), + currentOptionState: updatedDataMap, + }); + setSearchTerm(''); + } + } else { + setSearchTerm(rl.line); + } + }); + + let description; + const page = usePagination({ + items: optionState.options, + active, + renderItem({ item, isActive }) { + if (isActive && !Separator.isSeparator(item)) { + description = item.description; + } + + return formatRenderedItem(item, isActive, theme); + }, + pageSize, + loop, + }); + + return buildView({ + page, + theme, + prefix, + status, + pageSize, + errorMsg, + firstRender, + searchTerm, + description, + instructions, + currentOptions: optionState.options, + message: theme.style.message(config.message), + }); + }, +); diff --git a/yarn.lock b/yarn.lock index 910c021e8..ebfca2e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7833,6 +7833,7 @@ __metadata: "@types/yargs": "npm:^17.0.24" "@typescript-eslint/eslint-plugin": "npm:^7.4.0" "@typescript-eslint/parser": "npm:^7.4.0" + ansi-escapes: "npm:^7.0.0" asn1.js: "npm:^5.4.1" bignumber.js: "npm:^9.1.1" chai: "npm:^4.5.0" @@ -14712,6 +14713,15 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^7.0.0": + version: 7.0.0 + resolution: "ansi-escapes@npm:7.0.0" + dependencies: + environment: "npm:^1.0.0" + checksum: 2d0e2345087bd7ae6bf122b9cc05ee35560d40dcc061146edcdc02bc2d7c7c50143cd12a22e69a0b5c0f62b948b7bc9a4539ee888b80f5bd33cdfd82d01a70ab + languageName: node + linkType: hard + "ansi-regex@npm:^2.0.0": version: 2.1.1 resolution: "ansi-regex@npm:2.1.1" @@ -18093,6 +18103,13 @@ __metadata: languageName: node linkType: hard +"environment@npm:^1.0.0": + version: 1.1.0 + resolution: "environment@npm:1.1.0" + checksum: dd3c1b9825e7f71f1e72b03c2344799ac73f2e9ef81b78ea8b373e55db021786c6b9f3858ea43a436a2c4611052670ec0afe85bc029c384cc71165feee2f4ba6 + languageName: node + linkType: hard + "erc721a@npm:^4.2.3": version: 4.2.3 resolution: "erc721a@npm:4.2.3" From 9c0c4bbe21c260865b6c15a65fcb05c8037f0ed1 Mon Sep 17 00:00:00 2001 From: Danil Nemirovsky Date: Fri, 18 Oct 2024 12:23:58 +0100 Subject: [PATCH 12/26] feat: Add logging block hash (#4707) ### Description Add logging block hash so that we have both block height and block hash. When Scraper will report that it cannot retrieve block by hash, we'll be able to find its block height. ### Backward compatibility Yes ### Testing Run Scraper locally to see the log message --------- Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com> --- .../hyperlane-cosmos/src/providers/rpc/provider.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rust/main/chains/hyperlane-cosmos/src/providers/rpc/provider.rs b/rust/main/chains/hyperlane-cosmos/src/providers/rpc/provider.rs index f9ec3e975..20b6ac714 100644 --- a/rust/main/chains/hyperlane-cosmos/src/providers/rpc/provider.rs +++ b/rust/main/chains/hyperlane-cosmos/src/providers/rpc/provider.rs @@ -14,7 +14,7 @@ use tendermint_rpc::endpoint::block_results::Response as BlockResultsResponse; use tendermint_rpc::endpoint::tx; use tendermint_rpc::HttpClient; use time::OffsetDateTime; -use tracing::{debug, instrument, trace}; +use tracing::{debug, info, instrument, trace}; use hyperlane_core::{ ChainCommunicationError, ChainResult, ContractLocator, HyperlaneDomain, LogMeta, H256, U256, @@ -249,6 +249,9 @@ impl WasmRpcProvider for CosmosWasmRpcProvider { // The two calls below could be made in parallel, but on cosmos rate limiting is a bigger problem // than indexing latency, so we do them sequentially. let block = self.rpc_client.get_block(block_number).await?; + + debug!(?block_number, block_hash = ?block.block_id.hash, cursor_label, domain=?self.domain, "Getting logs in block with hash"); + let block_results = self.rpc_client.get_block_results(block_number).await?; Ok(self.handle_txs(block, block_results, parser, cursor_label)) @@ -268,7 +271,12 @@ impl WasmRpcProvider for CosmosWasmRpcProvider { debug!(?hash, cursor_label, domain=?self.domain, "Getting logs in transaction"); let tx = self.rpc_client.get_tx_by_hash(hash).await?; - let block = self.rpc_client.get_block(tx.height.value() as u32).await?; + + let block_number = tx.height.value() as u32; + let block = self.rpc_client.get_block(block_number).await?; + + debug!(?block_number, block_hash = ?block.block_id.hash, cursor_label, domain=?self.domain, "Getting logs in transaction: block info"); + let block_hash = H256::from_slice(block.block_id.hash.as_bytes()); Ok(self.handle_tx(tx, block_hash, parser).collect()) From a54210583e73e9a994e2897722866e72166c34f7 Mon Sep 17 00:00:00 2001 From: Mohammed Hussan Date: Fri, 18 Oct 2024 15:51:08 +0100 Subject: [PATCH 13/26] feat(warpMonitor): Add support for sei-FASTUSD warp monitor (#4702) ### Description - Support sei-ethereum-FastUSD warp monitor - Support xERC20 balance metrics ### Testing Manual --- .../mainnet3/warp/EZETH-deployments.yaml | 2 +- .../warp/sei-FASTUSD-deployments.yaml | 21 ++ .../monitor-warp-routes-balances.ts | 199 +++++++++++------- 3 files changed, 150 insertions(+), 72 deletions(-) create mode 100644 typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml diff --git a/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml index bc90c54bb..b83663645 100644 --- a/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml @@ -9,7 +9,7 @@ data: name: Renzo Restaked ETH symbol: ezETH hypAddress: '0xC59336D8edDa9722B4f1Ec104007191Ec16f7087' - tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + tokenAddress: '0xbf5495Efe5DB9ce00f80364C8B423567e58d2110' decimals: 18 bsc: protocolType: ethereum diff --git a/typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml new file mode 100644 index 000000000..885c1a4db --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml @@ -0,0 +1,21 @@ +description: Hyperlane Warp Route artifacts +timestamp: '2024-10-17T14:00:00.000Z' +deployer: Abacus Works (Hyperlane) +data: + config: + ethereum: + protocolType: ethereum + type: collateral + hypAddress: '0x9AD81058c6C3Bf552C9014CB30E824717A0ee21b' + tokenAddress: '0x15700B564Ca08D9439C58cA5053166E8317aa138' + name: fastUSD + symbol: fastUSD + decimals: 18 + sei: + protocolType: ethereum + type: xERC20 + hypAddress: '0xeA895A7Ff45d8d3857A04c1E38A362f3bd9a076f' + tokenAddress: '0x37a4dD9CED2b19Cfe8FAC251cd727b5787E45269' + name: fastUSD + symbol: fastUSD + decimals: 18 diff --git a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts index 160a777f5..3d946a9e6 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -55,10 +55,11 @@ const xERC20LimitsGauge = new Gauge({ name: 'hyperlane_xerc20_limits', help: 'Current minting and burning limits of xERC20 tokens', registers: [metricsRegister], - labelNames: ['chain_name', 'limit_type'], + labelNames: ['chain_name', 'limit_type', 'token_name'], }); interface xERC20Limit { + tokenName: string; mint: number; burn: number; mintMax: number; @@ -102,18 +103,7 @@ async function main(): Promise { const registry = await envConfig.getRegistry(); const chainMetadata = await registry.getMetadata(); - // TODO: eventually support token balance checks for xERC20 token type also - if ( - Object.values(tokenConfig).some( - (token) => - token.type === TokenType.XERC20 || - token.type === TokenType.XERC20Lockbox, - ) - ) { - await checkXERC20Limits(checkFrequency, tokenConfig, chainMetadata); - } else { - await checkTokenBalances(checkFrequency, tokenConfig, chainMetadata); - } + await checkWarpRouteMetrics(checkFrequency, tokenConfig, chainMetadata); return true; } @@ -136,7 +126,7 @@ async function checkBalance( ethers.utils.formatUnits(nativeBalance, token.decimals), ); } - case ProtocolType.Sealevel: + case ProtocolType.Sealevel: { const adapter = new SealevelHypNativeAdapter( chain, multiProtocolProvider, @@ -155,6 +145,7 @@ async function checkBalance( return parseFloat( ethers.utils.formatUnits(balance, token.decimals), ); + } case ProtocolType.Cosmos: { if (!token.ibcDenom) throw new Error('IBC denom missing for native token'); @@ -245,7 +236,7 @@ async function checkBalance( ethers.utils.formatUnits(syntheticBalance, token.decimals), ); } - case ProtocolType.Sealevel: + case ProtocolType.Sealevel: { if (!token.tokenAddress) throw new Error('Token address missing for synthetic token'); const adapter = new SealevelHypSyntheticAdapter( @@ -265,12 +256,67 @@ async function checkBalance( return parseFloat( ethers.utils.formatUnits(syntheticBalance, token.decimals), ); + } case ProtocolType.Cosmos: // TODO - cosmos synthetic return 0; } break; } + case TokenType.XERC20: { + switch (token.protocolType) { + case ProtocolType.Ethereum: { + const provider = multiProtocolProvider.getEthersV5Provider(chain); + const hypXERC20 = HypXERC20__factory.connect( + token.hypAddress, + provider, + ); + const xerc20Address = await hypXERC20.wrappedToken(); + const xerc20 = IXERC20__factory.connect(xerc20Address, provider); + const syntheticBalance = await xerc20.totalSupply(); + + return parseFloat( + ethers.utils.formatUnits(syntheticBalance, token.decimals), + ); + } + default: + throw new Error( + `Unsupported protocol type ${token.protocolType} for token type ${token.type}`, + ); + } + } + case TokenType.XERC20Lockbox: { + switch (token.protocolType) { + case ProtocolType.Ethereum: { + if (!token.tokenAddress) + throw new Error( + 'Token address missing for xERC20Lockbox token', + ); + const provider = multiProtocolProvider.getEthersV5Provider(chain); + const hypXERC20Lockbox = HypXERC20Lockbox__factory.connect( + token.hypAddress, + provider, + ); + const xerc20LockboxAddress = await hypXERC20Lockbox.lockbox(); + const tokenContract = ERC20__factory.connect( + token.tokenAddress, + provider, + ); + + const collateralBalance = await tokenContract.balanceOf( + xerc20LockboxAddress, + ); + + return parseFloat( + ethers.utils.formatUnits(collateralBalance, token.decimals), + ); + } + default: + throw new Error( + `Unsupported protocol type ${token.protocolType} for token type ${token.type}`, + ); + } + } } return 0; }, @@ -301,46 +347,51 @@ export function updateTokenBalanceMetrics( }); } -export function updateXERC20LimitsMetrics(xERC20Limits: ChainMap) { - objMap(xERC20Limits, (chain: ChainName, limit: xERC20Limit) => { - xERC20LimitsGauge - .labels({ - chain_name: chain, - limit_type: 'mint', - }) - .set(limit.mint); - xERC20LimitsGauge - .labels({ - chain_name: chain, - limit_type: 'burn', - }) - .set(limit.burn); - xERC20LimitsGauge - .labels({ - chain_name: chain, - limit_type: 'mintMax', - }) - .set(limit.mintMax); - xERC20LimitsGauge - .labels({ - chain_name: chain, - limit_type: 'burnMax', - }) - .set(limit.burnMax); - logger.info('xERC20 limits updated for chain', { - chain, - mint: limit.mint, - burn: limit.burn, - mintMax: limit.mintMax, - burnMax: limit.burnMax, - }); +export function updateXERC20LimitsMetrics( + xERC20Limits: ChainMap, +) { + objMap(xERC20Limits, (chain: ChainName, limits: xERC20Limit | undefined) => { + if (limits) { + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'mint', + token_name: limits.tokenName, + }) + .set(limits.mint); + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'burn', + token_name: limits.tokenName, + }) + .set(limits.burn); + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'mintMax', + token_name: limits.tokenName, + }) + .set(limits.mintMax); + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'burnMax', + token_name: limits.tokenName, + }) + .set(limits.burnMax); + logger.info('xERC20 limits updated for chain', { + chain, + limits, + }); + } }); } async function getXERC20Limits( tokenConfig: WarpRouteConfig, chainMetadata: ChainMap, -): Promise> { +): Promise> { const multiProtocolProvider = new MultiProtocolProvider(chainMetadata); const output = objMap( @@ -358,7 +409,12 @@ async function getXERC20Limits( ); const xerc20Address = await lockbox.xERC20(); const xerc20 = IXERC20__factory.connect(xerc20Address, provider); - return getXERC20Limit(routerAddress, xerc20, token.decimals); + return getXERC20Limit( + routerAddress, + xerc20, + token.decimals, + token.name, + ); } case TokenType.XERC20: { const provider = multiProtocolProvider.getEthersV5Provider(chain); @@ -369,10 +425,19 @@ async function getXERC20Limits( ); const xerc20Address = await hypXERC20.wrappedToken(); const xerc20 = IXERC20__factory.connect(xerc20Address, provider); - return getXERC20Limit(routerAddress, xerc20, token.decimals); + return getXERC20Limit( + routerAddress, + xerc20, + token.decimals, + token.name, + ); } default: - throw new Error(`Unsupported token type ${token.type}`); + logger.info( + `Unsupported token type ${token.type} for xERC20 limits check on protocol type ${token.protocolType}`, + ); + + return undefined; } } default: @@ -388,12 +453,14 @@ const getXERC20Limit = async ( routerAddress: string, xerc20: IXERC20, decimals: number, + tokenName: string, ): Promise => { const mintCurrent = await xerc20.mintingCurrentLimitOf(routerAddress); const mintMax = await xerc20.mintingMaxLimitOf(routerAddress); const burnCurrent = await xerc20.burningCurrentLimitOf(routerAddress); const burnMax = await xerc20.burningMaxLimitOf(routerAddress); return { + tokenName, mint: parseFloat(ethers.utils.formatUnits(mintCurrent, decimals)), mintMax: parseFloat(ethers.utils.formatUnits(mintMax, decimals)), burn: parseFloat(ethers.utils.formatUnits(burnCurrent, decimals)), @@ -401,37 +468,27 @@ const getXERC20Limit = async ( }; }; -async function checkXERC20Limits( +async function checkWarpRouteMetrics( checkFrequency: number, tokenConfig: WarpRouteConfig, chainMetadata: ChainMap, ) { setInterval(async () => { try { - const xERC20Limits = await getXERC20Limits(tokenConfig, chainMetadata); - logger.info('xERC20 Limits:', xERC20Limits); - updateXERC20LimitsMetrics(xERC20Limits); + const multiProtocolProvider = new MultiProtocolProvider(chainMetadata); + const balances = await checkBalance(tokenConfig, multiProtocolProvider); + logger.info('Token Balances:', balances); + updateTokenBalanceMetrics(tokenConfig, balances); } catch (e) { logger.error('Error checking balances', e); } - }, checkFrequency); -} - -async function checkTokenBalances( - checkFrequency: number, - tokenConfig: WarpRouteConfig, - chainMetadata: ChainMap, -) { - logger.info('Starting Warp Route balance monitor'); - const multiProtocolProvider = new MultiProtocolProvider(chainMetadata); - setInterval(async () => { try { - logger.debug('Checking balances'); - const balances = await checkBalance(tokenConfig, multiProtocolProvider); - updateTokenBalanceMetrics(tokenConfig, balances); + const xERC20Limits = await getXERC20Limits(tokenConfig, chainMetadata); + logger.info('xERC20 Limits:', xERC20Limits); + updateXERC20LimitsMetrics(xERC20Limits); } catch (e) { - logger.error('Error checking balances', e); + logger.error('Error checking xERC20 limits', e); } }, checkFrequency); } From 41035aac8f75dc10fe7b2a50f4451b1a4bdf593b Mon Sep 17 00:00:00 2001 From: Lee <6251863+ltyu@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:40:14 -0400 Subject: [PATCH 14/26] fix: early strategy detection in warp apply (#4706) ### Description When using `warp apply`, a user may provide a `strategyUrl`. However, they won't know if 1) it's valid, and 2) if it even exists until much later. This adds the detection & validation early. ### Drive-by changes Remove yaml print ### Backward compatibility Yes ### Testing Manual --- .changeset/dry-foxes-battle.md | 6 ++++++ typescript/cli/src/commands/warp.ts | 4 ++++ typescript/cli/src/deploy/warp.ts | 5 ----- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/dry-foxes-battle.md diff --git a/.changeset/dry-foxes-battle.md b/.changeset/dry-foxes-battle.md new file mode 100644 index 000000000..eadb8427e --- /dev/null +++ b/.changeset/dry-foxes-battle.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Add strategyUrl detect and validation in the beginning of `warp apply` +Remove yaml transactions print from `warp apply` diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index d3bee1e00..107db6850 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -9,6 +9,7 @@ import { } from '@hyperlane-xyz/core'; import { ChainMap, + ChainSubmissionStrategySchema, EvmERC20WarpRouteReader, TokenStandard, WarpCoreConfig, @@ -30,6 +31,7 @@ import { log, logGray, logGreen, logRed, logTable } from '../logger.js'; import { sendTestTransfer } from '../send/transfer.js'; import { indentYamlOrJson, + readYamlOrJson, removeEndingSlash, writeYamlOrJson, } from '../utils/files.js'; @@ -113,6 +115,8 @@ export const apply: CommandModuleWithWriteContext<{ logRed(`Please specify either a symbol or warp config`); process.exit(0); } + if (strategyUrl) + ChainSubmissionStrategySchema.parse(readYamlOrJson(strategyUrl)); const warpDeployConfig = await readWarpRouteDeployConfig(config); await runWarpRouteApply({ diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 872edb400..36bbc2ad8 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -848,11 +848,6 @@ async function submitWarpApplyTransactions( `Transactions receipts successfully written to ${receiptPath}`, ); } - - logGreen( - `✅ Warp route update success with ${submitter.txSubmitterType} on ${chain}:\n\n`, - indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 0), - ); }), ); } From 0b901886cb745c944cb107e9df55164863d0357a Mon Sep 17 00:00:00 2001 From: Mohammed Hussan Date: Fri, 18 Oct 2024 17:13:12 +0100 Subject: [PATCH 15/26] feat(warpMonitor): Support Lumia bsc-lumia-ethereum warp monitor service (#4709) ### Description - Support Lumia bsc-lumia-ethereum warp monitor service ### Testing Manual --- .../warp/bsc-lumia-LUMIA-deployments.yaml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml diff --git a/typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml new file mode 100644 index 000000000..9db075248 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml @@ -0,0 +1,29 @@ +# Configs and artifacts for the deployment of Hyperlane Warp Routes +# Between Ethereum and Binance Smart Chain and Lumia +description: Hyperlane Warp Route artifacts +timestamp: '2024-10-18T14:00:00.000Z' +deployer: Abacus Works (Hyperlane) +data: + config: + ethereum: + protocolType: ethereum + type: collateral + hypAddress: '0xdD313D475f8A9d81CBE2eA953a357f52e10BA357' + tokenAddress: '0xd9343a049d5dbd89cd19dc6bca8c48fb3a0a42a7' + name: Lumia Token + symbol: LUMIA + decimals: 18 + bsc: + protocolType: ethereum + type: synthetic + hypAddress: '0x7F39BcdCa8E0E581c1d43aaa1cB862AA1c8C2047' + name: Lumia Token + symbol: LUMIA + decimals: 18 + lumia: + protocolType: ethereum + type: native + hypAddress: '0x6a77331cE28E47c3Cb9Fea48AB6cD1e9594ce0A9' + name: Lumia Token + symbol: LUMIA + decimals: 18 From eeae55bf95003fd440f0c19008024d6a789ceef8 Mon Sep 17 00:00:00 2001 From: Danil Nemirovsky Date: Mon, 21 Oct 2024 10:30:04 +0100 Subject: [PATCH 16/26] feat: Upgrade scraper (#4708) ### Description Add logging for debugging ### Backward compatibility Yes ### Testing Locally Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com> --- typescript/infra/config/environments/mainnet3/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index a470b1c87..6b6f2bae5 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -437,7 +437,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: 'efd438f-20241016-101828', + tag: '9c0c4bb-20241018-113820', }, resources: scraperResources, }, From 02a5b92ba774c3ce8c706966c9b9f23272215fa5 Mon Sep 17 00:00:00 2001 From: Paul Balaji <10051819+paulbalaji@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:57:44 +0100 Subject: [PATCH 17/26] feat: enroll new chains + validators on default ISMs (#4694) ### Description feat: enroll new chains + validators on default ISMs - migrate new chains to ICAs - migrate some existing SAFE owned chains to ICAs - ensure defaultHook on inevm is made hot again - add J to safe signers ### Drive-by changes - IGP gas/token price updates - add tx overrides when deploying ICA accounts - swap out A for J on safe signers - add support for updating owners when doing a safe multisend - add support for checking and surfacing defaulthook/requiredhook owner violations - improve logging + skip bytecode mismatches for now - parallelise call inference in base app governor - parallelise deployment of ICAs in get-owner-ica.ts - reuse existing configured gasPrices for sealevel chains ### Related issues na ### Backward compatibility yes ### Testing will be testing as part of check-deploy --- .changeset/few-goats-add.md | 5 + .../config/environments/mainnet3/chains.ts | 7 +- .../environments/mainnet3/gasPrices.json | 28 ++-- .../config/environments/mainnet3/owners.ts | 40 ++++- .../mainnet3/safe/safeSigners.json | 2 +- .../environments/mainnet3/tokenPrices.json | 138 +++++++++--------- typescript/infra/scripts/get-owner-ica.ts | 57 ++++++-- typescript/infra/scripts/print-gas-prices.ts | 24 ++- .../infra/src/govern/HyperlaneAppGovernor.ts | 55 +++++-- .../infra/src/govern/HyperlaneCoreGovernor.ts | 6 +- .../infra/src/govern/HyperlaneIgpGovernor.ts | 6 +- typescript/infra/src/utils/safe.ts | 54 ++++++- typescript/sdk/src/consts/multisigIsm.ts | 80 +++++++--- .../sdk/src/core/HyperlaneCoreChecker.ts | 49 ++++++- .../sdk/src/deploy/HyperlaneAppChecker.ts | 7 +- .../middleware/account/InterchainAccount.ts | 3 + 16 files changed, 411 insertions(+), 150 deletions(-) create mode 100644 .changeset/few-goats-add.md diff --git a/.changeset/few-goats-add.md b/.changeset/few-goats-add.md new file mode 100644 index 000000000..ffcab1f49 --- /dev/null +++ b/.changeset/few-goats-add.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Enroll new validators. Add tx overrides when deploying ICA accounts. Core checker now surfaces owner violations for defaultHook and requiredHook. App checker temporarily ignores bytecode mismatch violations. diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index e4d7f774f..b32fdeff1 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -59,12 +59,17 @@ export const chainMetadataOverrides: ChainMap> = { // gasLimit: 6800000, // set when deploying contracts }, }, - // set when deploying contracts + // Deploy-only overrides, set when deploying contracts // chiliz: { // transactionOverrides: { // maxFeePerGas: 100000 * 10 ** 9, // 100,000 gwei // }, // }, + // zircuit: { + // blocks: { + // confirmations: 3, + // }, + // }, }; export const getRegistry = async (useSecrets = true): Promise => diff --git a/typescript/infra/config/environments/mainnet3/gasPrices.json b/typescript/infra/config/environments/mainnet3/gasPrices.json index 81e8ae1ef..d68a6d290 100644 --- a/typescript/infra/config/environments/mainnet3/gasPrices.json +++ b/typescript/infra/config/environments/mainnet3/gasPrices.json @@ -24,7 +24,7 @@ "decimals": 9 }, "base": { - "amount": "0.008669818", + "amount": "0.015396226", "decimals": 9 }, "bitlayer": { @@ -32,7 +32,7 @@ "decimals": 9 }, "blast": { - "amount": "0.004707204", + "amount": "0.005712307", "decimals": 9 }, "bob": { @@ -72,15 +72,15 @@ "decimals": 9 }, "eclipsemainnet": { - "amount": "0.001", - "decimals": 9 + "amount": "0.0000001", + "decimals": 1 }, "endurance": { "amount": "3.0756015", "decimals": 9 }, "ethereum": { - "amount": "21.610477208", + "amount": "14.852716956", "decimals": 9 }, "everclear": { @@ -88,11 +88,11 @@ "decimals": 9 }, "flare": { - "amount": "29.55878872", + "amount": "49.455765643", "decimals": 9 }, "flow": { - "amount": "0.0000001", + "amount": "0.1", "decimals": 9 }, "fraxtal": { @@ -104,11 +104,11 @@ "decimals": 9 }, "gnosis": { - "amount": "2.000000007", + "amount": "1.500000007", "decimals": 9 }, "immutablezkevm": { - "amount": "10.00000005", + "amount": "10.1", "decimals": 9 }, "inevm": { @@ -124,7 +124,7 @@ "decimals": 9 }, "linea": { - "amount": "0.240000007", + "amount": "0.243", "decimals": 9 }, "lisk": { @@ -156,7 +156,7 @@ "decimals": 9 }, "metis": { - "amount": "1.247735823", + "amount": "1.278943587", "decimals": 9 }, "mint": { @@ -236,12 +236,12 @@ "decimals": 9 }, "shibarium": { - "amount": "28.138673121", + "amount": "39.319461243", "decimals": 9 }, "solanamainnet": { - "amount": "0.001", - "decimals": 9 + "amount": "0.5", + "decimals": 1 }, "superposition": { "amount": "0.01", diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index 66cd8d17d..e32d8961c 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -68,13 +68,14 @@ export const icas: Partial< inevm: '0xFDF9EDcb2243D51f5f317b9CEcA8edD2bEEE036e', // Jul 26, 2024 batch - // ------------------------------------- + // ---------------------------------------------------------- xlayer: '0x1571c482fe9E76bbf50829912b1c746792966369', cheesechain: '0xEe2C5320BE9bC7A1492187cfb289953b53E3ff1b', worldchain: '0x1996DbFcFB433737fE404F58D2c32A7f5f334210', // zircuit: '0x0d67c56E818a02ABa58cd2394b95EF26db999aA3', // already has a safe // Aug 5, 2024 batch + // ---------------------------------------------------------- cyber: '0x984Fe5a45Ac4aaeC4E4655b50f776aB79c9Be19F', degenchain: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', kroma: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', @@ -88,9 +89,10 @@ export const icas: Partial< sanko: '0x5DAcd2f1AafC749F2935A160865Ab1568eC23752', tangle: '0xCC2aeb692197C7894E561d31ADFE8F79746f7d9F', xai: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', - // taiko: '0x483D218D2FEe7FC7204ba15F00C7901acbF9697D', // already has a safe + // taiko: '0x483D218D2FEe7FC7204ba15F00C7901acbF9697D', // renzo chain // Aug 26, 2024 batch + // ---------------------------------------------------------- astar: '0x6b241544eBa7d89B51b72DF85a0342dAa37371Ca', astarzkevm: '0x526c6DAee1175A1A2337E703B63593acb327Dde4', bitlayer: '0xe6239316cA60814229E801fF0B9DD71C9CA29008', @@ -101,9 +103,41 @@ export const icas: Partial< shibarium: '0x6348FAe3a8374dbAAaE84EEe5458AE4063Fe2be7', // Sep 9, 2024 batch - // ---------------------------- + // ---------------------------------------------------------- everclear: '0x63B2075372D6d0565F51210D0A296D3c8a773aB6', oortmainnet: '0x7021D11F9fAe455AB2f45D72dbc2C64d116Cb657', + + // Sep 19, 2024 SAFE --> ICA v1 Migration + // ---------------------------------------------------------- + celo: '0x3fA264c58E1365f1d5963B831b864EcdD2ddD19b', + avalanche: '0x8c8695cD9905e22d84E466804ABE55408A87e595', + polygon: '0xBDD25dd5203fedE33FD631e30fEF9b9eF2598ECE', + moonbeam: '0x480e5b5De6a29F07fe8295C60A1845d36b7BfdE6', + gnosis: '0xD42125a4889A7A36F32d7D12bFa0ae52B0AD106b', + scroll: '0x2a3fe2513F4A7813683d480724AB0a3683EfF8AC', + polygonzkevm: '0x66037De438a59C966214B78c1d377c4e93a5C7D1', + ancient8: '0xA9FD5BeB556AB1859D7625B381110a257f56F98C', + redstone: '0x5DAcd2f1AafC749F2935A160865Ab1568eC23752', + mantle: '0x08C880b88335CA3e85Ebb4E461245a7e899863c9', + bob: '0xc99e58b9A4E330e2E4d09e2c94CD3c553904F588', + zetachain: '0xc876B8e63c3ff5b636d9492715BE375644CaD345', + zoramainnet: '0x84977Eb15E0ff5824a6129c789F70e88352C230b', + fusemainnet: '0xbBdb1682B2922C282b56DD716C29db5EFbdb5632', + endurance: '0x470E04D8a3b7938b385093B93CeBd8Db7A1E557C', + // sei: '0xabad187003EdeDd6C720Fc633f929EA632996567', // renzo chain + + // Oct 16, 2024 batch + // ---------------------------------------------------------- + immutablezkevm: '0x8483e1480B62cB9f0aCecEbF42469b9f4013577a', + rari: '0x1124D54E989570A798769E534eAFbE1444f40AF6', + rootstock: '0x69350aeA98c5195F2c3cC6E6A065d0d8B12F659A', + alephzeroevm: '0x004a4C2e4Cd4F5Bd564fe0A6Ab2Da56258aE576f', + chiliz: '0xb52D281aD2BA9761c16f400d755837493e2baDB7', + lumia: '0x418E10Ac9e0b84022d0636228d05bc74172e0e41', + superposition: '0x34b57ff8fBA8da0cFdA795CC0F874FfaB14B1DE9', + flow: '0xf48377f8A3ddA7AAD7C2460C81d939434c829b45', + metall2: '0x2f1b1B0Fb7652E621316460f6c3b019F61d8dC9a', + polynomial: '0xC20eFa1e5A378af9233e9b24515eb3408d43f900', } as const; export const DEPLOYER = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; diff --git a/typescript/infra/config/environments/mainnet3/safe/safeSigners.json b/typescript/infra/config/environments/mainnet3/safe/safeSigners.json index d66b992ab..fca35815f 100644 --- a/typescript/infra/config/environments/mainnet3/safe/safeSigners.json +++ b/typescript/infra/config/environments/mainnet3/safe/safeSigners.json @@ -3,7 +3,7 @@ "0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba", "0xc3E966E79eF1aA4751221F55fB8A36589C24C0cA", "0x3b7f8f68A4FD0420FeA2F42a1eFc53422f205599", - "0x88436919fAa2310d32A36D20d13E0a441D24fAc3", + "0x478be6076f31E9666123B9721D0B6631baD944AF", "0x003DDD9eEAb62013b7332Ab4CC6B10077a8ca961", "0xd00d6A31485C93c597D1d8231eeeE0ed17B9844B", "0x483fd7284A696343FEc0819DDF2cf7E06E8A06E5", diff --git a/typescript/infra/config/environments/mainnet3/tokenPrices.json b/typescript/infra/config/environments/mainnet3/tokenPrices.json index 5cbf8f157..df245702e 100644 --- a/typescript/infra/config/environments/mainnet3/tokenPrices.json +++ b/typescript/infra/config/environments/mainnet3/tokenPrices.json @@ -1,73 +1,73 @@ { - "ancient8": "2437.96", - "alephzeroevm": "0.36741", - "arbitrum": "2437.96", - "astar": "0.059165", - "astarzkevm": "2437.96", - "avalanche": "26.77", - "base": "2437.96", - "bitlayer": "62244", - "blast": "2437.96", - "bob": "2437.96", - "bsc": "572.12", - "celo": "0.764821", - "cheesechain": "0.00448064", - "chiliz": "0.069844", - "coredao": "0.912209", - "cyber": "2437.96", - "degenchain": "0.00934571", - "dogechain": "0.110085", - "eclipsemainnet": "2437.96", - "endurance": "2.11", - "ethereum": "2437.96", - "everclear": "2437.96", - "flare": "0.01456139", - "flow": "0.533589", - "fraxtal": "2434.37", - "fusemainnet": "0.02952521", - "gnosis": "1.009", - "immutablezkevm": "1.48", - "inevm": "20.24", - "injective": "20.24", - "kroma": "2437.96", - "linea": "2437.96", - "lisk": "2437.96", - "lukso": "1.51", - "lumia": "0.954153", - "mantapacific": "2437.96", - "mantle": "0.59813", - "merlin": "62293", - "metall2": "2437.96", - "metis": "34.53", - "mint": "2437.96", - "mode": "2437.96", - "molten": "0.632429", - "moonbeam": "0.163919", - "neutron": "0.390086", - "oortmainnet": "0.11645", - "optimism": "2437.96", - "osmosis": "0.521323", - "polygon": "0.371959", - "polygonzkevm": "2437.96", - "polynomial": "2437.96", - "proofofplay": "2437.96", - "rari": "2437.96", + "ancient8": "2629.74", + "alephzeroevm": "0.381786", + "arbitrum": "2629.74", + "astar": "0.061114", + "astarzkevm": "2629.74", + "avalanche": "27.96", + "base": "2629.74", + "bitlayer": "67813", + "blast": "2629.74", + "bob": "2629.74", + "bsc": "597.89", + "celo": "0.817141", + "cheesechain": "0.00556724", + "chiliz": "0.079288", + "coredao": "0.98348", + "cyber": "2629.74", + "degenchain": "0.00882961", + "dogechain": "0.126177", + "eclipsemainnet": "2629.74", + "endurance": "2.16", + "ethereum": "2629.74", + "everclear": "2629.74", + "flare": "0.01493582", + "flow": "0.558323", + "fraxtal": "2629.35", + "fusemainnet": "0.02901498", + "gnosis": "0.997404", + "immutablezkevm": "1.54", + "inevm": "21.05", + "injective": "21.05", + "kroma": "2629.74", + "linea": "2629.74", + "lisk": "2629.74", + "lukso": "1.47", + "lumia": "0.969511", + "mantapacific": "2629.74", + "mantle": "0.636484", + "merlin": "67781", + "metall2": "2629.74", + "metis": "45.78", + "mint": "2629.74", + "mode": "2629.74", + "molten": "0.436605", + "moonbeam": "0.169406", + "neutron": "0.408859", + "oortmainnet": "0.114304", + "optimism": "2629.74", + "osmosis": "0.558566", + "polygon": "0.371646", + "polygonzkevm": "2629.74", + "polynomial": "2629.74", + "proofofplay": "2629.74", + "rari": "2629.74", "real": "1", - "redstone": "2437.96", - "rootstock": "61812", - "sanko": "41.59", - "scroll": "2437.96", - "sei": "0.444401", - "shibarium": "0.404651", - "solanamainnet": "144.84", - "superposition": "2437.96", - "taiko": "2437.96", + "redstone": "2629.74", + "rootstock": "67219", + "sanko": "70.7", + "scroll": "2629.74", + "sei": "0.447635", + "shibarium": "0.410927", + "solanamainnet": "155.35", + "superposition": "2629.74", + "taiko": "2629.74", "tangle": "1", - "viction": "0.359062", - "worldchain": "2437.96", - "xai": "0.215315", - "xlayer": "42.29", - "zetachain": "0.581304", - "zircuit": "2437.96", - "zoramainnet": "2437.96" + "viction": "0.369839", + "worldchain": "2629.74", + "xai": "0.216438", + "xlayer": "41.56", + "zetachain": "0.617959", + "zircuit": "2629.74", + "zoramainnet": "2629.74" } diff --git a/typescript/infra/scripts/get-owner-ica.ts b/typescript/infra/scripts/get-owner-ica.ts index e4ea61a7b..91c7ec9a8 100644 --- a/typescript/infra/scripts/get-owner-ica.ts +++ b/typescript/infra/scripts/get-owner-ica.ts @@ -1,11 +1,13 @@ import { AccountConfig, InterchainAccount } from '@hyperlane-xyz/sdk'; -import { Address, assert, eqAddress } from '@hyperlane-xyz/utils'; +import { Address, eqAddress } from '@hyperlane-xyz/utils'; -import { getArgs as getEnvArgs, withChainsRequired } from './agent-utils.js'; +import { isEthereumProtocolChain } from '../src/utils/utils.js'; + +import { getArgs as getEnvArgs, withChains } from './agent-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from './core-utils.js'; function getArgs() { - return withChainsRequired(getEnvArgs()) + return withChains(getEnvArgs()) .option('ownerChain', { type: 'string', description: 'Origin chain where the governing owner lives', @@ -51,20 +53,47 @@ async function main() { owner: originOwner, }; + const getOwnerIcaChains = ( + chains?.length ? chains : config.supportedChainNames + ).filter(isEthereumProtocolChain); + const results: Record = {}; - for (const chain of chains) { - const account = await ica.getAccount(chain, ownerConfig); - results[chain] = { ICA: account }; + const settledResults = await Promise.allSettled( + getOwnerIcaChains.map(async (chain) => { + try { + const account = await ica.getAccount(chain, ownerConfig); + const result: { ICA: Address; Deployed?: string } = { ICA: account }; + + if (deploy) { + const deployedAccount = await ica.deployAccount(chain, ownerConfig); + result.Deployed = eqAddress(account, deployedAccount) ? '✅' : '❌'; + if (result.Deployed === '❌') { + console.warn( + `Mismatch between account and deployed account for ${chain}`, + ); + } + } - if (deploy) { - const deployedAccount = await ica.deployAccount(chain, ownerConfig); - assert( - eqAddress(account, deployedAccount), - 'Fatal mismatch between account and deployed account', - ); - results[chain].Deployed = '✅'; + return { chain, result }; + } catch (error) { + console.error(`Error processing chain ${chain}:`, error); + return { chain, error }; + } + }), + ); + + settledResults.forEach((settledResult) => { + if (settledResult.status === 'fulfilled') { + const { chain, result, error } = settledResult.value; + if (error || !result) { + console.error(`Failed to process ${chain}:`, error); + } else { + results[chain] = result; + } + } else { + console.error(`Promise rejected:`, settledResult.reason); } - } + }); console.table(results); } diff --git a/typescript/infra/scripts/print-gas-prices.ts b/typescript/infra/scripts/print-gas-prices.ts index 805122ad1..bbe15c7b1 100644 --- a/typescript/infra/scripts/print-gas-prices.ts +++ b/typescript/infra/scripts/print-gas-prices.ts @@ -7,8 +7,10 @@ import { ProtocolType } from '@hyperlane-xyz/utils'; // Intentionally circumvent `mainnet3/index.ts` and `getEnvironmentConfig('mainnet3')` // to avoid circular dependencies. import { getRegistry as getMainnet3Registry } from '../config/environments/mainnet3/chains.js'; +import mainnet3GasPrices from '../config/environments/mainnet3/gasPrices.json' assert { type: 'json' }; import { supportedChainNames as mainnet3SupportedChainNames } from '../config/environments/mainnet3/supportedChainNames.js'; import { getRegistry as getTestnet4Registry } from '../config/environments/testnet4/chains.js'; +import testnet4GasPrices from '../config/environments/testnet4/gasPrices.json' assert { type: 'json' }; import { supportedChainNames as testnet4SupportedChainNames } from '../config/environments/testnet4/supportedChainNames.js'; import { GasPriceConfig, @@ -19,15 +21,17 @@ import { getArgs } from './agent-utils.js'; async function main() { const { environment } = await getArgs().argv; - const { registry, supportedChainNames } = + const { registry, supportedChainNames, gasPrices } = environment === 'mainnet3' ? { registry: await getMainnet3Registry(), supportedChainNames: mainnet3SupportedChainNames, + gasPrices: mainnet3GasPrices, } : { registry: await getTestnet4Registry(), supportedChainNames: testnet4SupportedChainNames, + gasPrices: testnet4GasPrices, }; const chainMetadata = await registry.getMetadata(); @@ -37,7 +41,11 @@ async function main() { await Promise.all( supportedChainNames.map(async (chain) => [ chain, - await getGasPrice(mpp, chain), + await getGasPrice( + mpp, + chain, + gasPrices[chain as keyof typeof gasPrices], + ), ]), ), ); @@ -48,6 +56,7 @@ async function main() { async function getGasPrice( mpp: MultiProtocolProvider, chain: string, + currentGasPrice?: GasPriceConfig, ): Promise { const protocolType = mpp.getProtocol(chain); switch (protocolType) { @@ -68,11 +77,14 @@ async function getGasPrice( }; } case ProtocolType.Sealevel: + // Return the gas price from the config if it exists, otherwise return some default // TODO get a reasonable value - return { - amount: '0.001', - decimals: 9, - }; + return ( + currentGasPrice ?? { + amount: 'PLEASE SET A GAS PRICE FOR SEALEVEL', + decimals: 1, + } + ); default: throw new Error(`Unsupported protocol type: ${protocolType}`); } diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts index 6df9d3551..d35a1499f 100644 --- a/typescript/infra/src/govern/HyperlaneAppGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts @@ -26,6 +26,8 @@ import { retryAsync, } from '@hyperlane-xyz/utils'; +import { getSafeAndService, updateSafeOwner } from '../utils/safe.js'; + import { ManualMultiSend, MultiSend, @@ -159,7 +161,22 @@ export abstract class HyperlaneAppGovernor< submissionType: SubmissionType, multiSend: MultiSend, ) => { - const callsForSubmissionType = filterCalls(submissionType) || []; + const callsForSubmissionType = []; + const filteredCalls = filterCalls(submissionType); + + // If calls are being submitted via a safe, we need to check for any safe owner changes first + if (submissionType === SubmissionType.SAFE) { + const { safeSdk } = await getSafeAndService( + chain, + this.checker.multiProvider, + (multiSend as SafeMultiSend).safeAddress, + ); + const updateOwnerCalls = await updateSafeOwner(safeSdk); + callsForSubmissionType.push(...updateOwnerCalls, ...filteredCalls); + } else { + callsForSubmissionType.push(...filteredCalls); + } + if (callsForSubmissionType.length > 0) { this.printSeparator(); const confirmed = await summarizeCalls( @@ -257,7 +274,6 @@ export abstract class HyperlaneAppGovernor< protected async inferCallSubmissionTypes() { const newCalls: ChainMap = {}; - const pushNewCall = (inferredCall: InferredCall) => { newCalls[inferredCall.chain] = newCalls[inferredCall.chain] || []; newCalls[inferredCall.chain].push({ @@ -267,20 +283,29 @@ export abstract class HyperlaneAppGovernor< }); }; - for (const chain of Object.keys(this.calls)) { - try { - for (const call of this.calls[chain]) { - const inferredCall = await this.inferCallSubmissionType(chain, call); - pushNewCall(inferredCall); + const results: ChainMap = {}; + await Promise.all( + Object.keys(this.calls).map(async (chain) => { + try { + results[chain] = await Promise.all( + this.calls[chain].map((call) => + this.inferCallSubmissionType(chain, call), + ), + ); + } catch (error) { + console.error( + chalk.red( + `Error inferring call submission types for chain ${chain}: ${error}`, + ), + ); + results[chain] = []; } - } catch (error) { - console.error( - chalk.red( - `Error inferring call submission types for chain ${chain}: ${error}`, - ), - ); - } - } + }), + ); + + Object.entries(results).forEach(([_, inferredCalls]) => { + inferredCalls.forEach(pushNewCall); + }); this.calls = newCalls; } diff --git a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts index b6df69f4f..f4b9c7455 100644 --- a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts @@ -84,7 +84,11 @@ export class HyperlaneCoreGovernor extends HyperlaneAppGovernor< return this.handleProxyAdminViolation(violation as ProxyAdminViolation); } default: - throw new Error(`Unsupported violation type ${violation.type}`); + throw new Error( + `Unsupported violation type ${violation.type}: ${JSON.stringify( + violation, + )}`, + ); } } } diff --git a/typescript/infra/src/govern/HyperlaneIgpGovernor.ts b/typescript/infra/src/govern/HyperlaneIgpGovernor.ts index 592301d9b..30528d230 100644 --- a/typescript/infra/src/govern/HyperlaneIgpGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneIgpGovernor.ts @@ -29,7 +29,11 @@ export class HyperlaneIgpGovernor extends HyperlaneAppGovernor< return super.handleOwnerViolation(violation as OwnerViolation); } default: - throw new Error(`Unsupported violation type ${violation.type}`); + throw new Error( + `Unsupported violation type ${violation.type}: ${JSON.stringify( + violation, + )}`, + ); } } diff --git a/typescript/infra/src/utils/safe.ts b/typescript/infra/src/utils/safe.ts index ad2104ddb..fc03e9366 100644 --- a/typescript/infra/src/utils/safe.ts +++ b/typescript/infra/src/utils/safe.ts @@ -6,7 +6,7 @@ import { SafeTransaction, } from '@safe-global/safe-core-sdk-types'; import chalk from 'chalk'; -import { ethers } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; import { ChainNameOrId, @@ -14,7 +14,10 @@ import { getSafe, getSafeService, } from '@hyperlane-xyz/sdk'; -import { Address, CallData } from '@hyperlane-xyz/utils'; +import { Address, CallData, eqAddress } from '@hyperlane-xyz/utils'; + +import safeSigners from '../../config/environments/mainnet3/safe/safeSigners.json' assert { type: 'json' }; +import { AnnotatedCallData } from '../govern/HyperlaneAppGovernor.js'; export async function getSafeAndService( chain: ChainNameOrId, @@ -222,3 +225,50 @@ export async function deleteSafeTx( ); } } + +export async function updateSafeOwner( + safeSdk: Safe.default, +): Promise { + const threshold = await safeSdk.getThreshold(); + const owners = await safeSdk.getOwners(); + const newOwners = safeSigners.signers; + const ownersToRemove = owners.filter( + (owner) => !newOwners.some((newOwner) => eqAddress(owner, newOwner)), + ); + const ownersToAdd = newOwners.filter( + (newOwner) => !owners.some((owner) => eqAddress(newOwner, owner)), + ); + + console.log(chalk.magentaBright('Owners to remove:', ownersToRemove)); + console.log(chalk.magentaBright('Owners to add:', ownersToAdd)); + + const transactions: AnnotatedCallData[] = []; + + for (const ownerToRemove of ownersToRemove) { + const { data: removeTxData } = await safeSdk.createRemoveOwnerTx({ + ownerAddress: ownerToRemove, + threshold, + }); + transactions.push({ + to: removeTxData.to, + data: removeTxData.data, + value: BigNumber.from(removeTxData.value), + description: `Remove safe owner ${ownerToRemove}`, + }); + } + + for (const ownerToAdd of ownersToAdd) { + const { data: addTxData } = await safeSdk.createAddOwnerTx({ + ownerAddress: ownerToAdd, + threshold, + }); + transactions.push({ + to: addTxData.to, + data: addTxData.data, + value: BigNumber.from(addTxData.value), + description: `Add safe owner ${ownerToAdd}`, + }); + } + + return transactions; +} diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index 0a8806bde..5184a4b9e 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -4,8 +4,12 @@ import { ChainMap } from '../types.js'; // TODO: consider migrating these to the registry too export const defaultMultisigConfigs: ChainMap = { alephzeroevm: { - threshold: 1, - validators: ['0xcae8fab142adc4e434bb7409e40dd932cc3851aa'], + threshold: 2, + validators: [ + '0xcae8fab142adc4e434bb7409e40dd932cc3851aa', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, alfajores: { @@ -175,8 +179,12 @@ export const defaultMultisigConfigs: ChainMap = { }, chiliz: { - threshold: 1, - validators: ['0x82d024f453b1a3f3f6606226f06b038da27596f3'], + threshold: 2, + validators: [ + '0x82d024f453b1a3f3f6606226f06b038da27596f3', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, citreatestnet: { @@ -286,8 +294,12 @@ export const defaultMultisigConfigs: ChainMap = { }, flow: { - threshold: 1, - validators: ['0x3aee1090318e9c54d1d23194dcd0f2bee00ddc97'], + threshold: 2, + validators: [ + '0x3aee1090318e9c54d1d23194dcd0f2bee00ddc97', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, formtestnet: { @@ -343,8 +355,12 @@ export const defaultMultisigConfigs: ChainMap = { }, immutablezkevm: { - threshold: 1, - validators: ['0xa787c2952a4d22f776ee6e87e828e6f75de24330'], + threshold: 2, + validators: [ + '0xa787c2952a4d22f776ee6e87e828e6f75de24330', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, inevm: { @@ -402,8 +418,12 @@ export const defaultMultisigConfigs: ChainMap = { }, lumia: { - threshold: 1, - validators: ['0x9e283254ed2cd2c80f007348c2822fc8e5c2fa5f'], + threshold: 2, + validators: [ + '0x9e283254ed2cd2c80f007348c2822fc8e5c2fa5f', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, mantapacific: { @@ -438,8 +458,12 @@ export const defaultMultisigConfigs: ChainMap = { }, metall2: { - threshold: 1, - validators: ['0x1b000e1e1f0a032ed382c6d69a2d58f6fe773c09'], + threshold: 2, + validators: [ + '0x1b000e1e1f0a032ed382c6d69a2d58f6fe773c09', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, metis: { @@ -567,8 +591,12 @@ export const defaultMultisigConfigs: ChainMap = { }, polynomial: { - threshold: 1, - validators: ['0xa63ad0891e921ad5947d57e05831fabb9816eca7'], + threshold: 2, + validators: [ + '0xa63ad0891e921ad5947d57e05831fabb9816eca7', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, proofofplay: { @@ -581,8 +609,12 @@ export const defaultMultisigConfigs: ChainMap = { }, rari: { - threshold: 1, - validators: ['0x989d6862e09de21337078efbd86843a3eb1133e3'], + threshold: 2, + validators: [ + '0x989d6862e09de21337078efbd86843a3eb1133e3', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, real: { @@ -604,8 +636,12 @@ export const defaultMultisigConfigs: ChainMap = { }, rootstock: { - threshold: 1, - validators: ['0xcb8e3a72cf427feff27416d0e2ec375a052eaaee'], + threshold: 2, + validators: [ + '0xcb8e3a72cf427feff27416d0e2ec375a052eaaee', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, sanko: { @@ -721,8 +757,12 @@ export const defaultMultisigConfigs: ChainMap = { }, superposition: { - threshold: 1, - validators: ['0x5978d0e6afa9270ddb87cff43a8fa7a763a5dfc4'], + threshold: 2, + validators: [ + '0x5978d0e6afa9270ddb87cff43a8fa7a763a5dfc4', + '0xCF0211faFBb91FD9D06D7E306B30032DC3A1934f', // merkly + '0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis + ], }, superpositiontestnet: { diff --git a/typescript/sdk/src/core/HyperlaneCoreChecker.ts b/typescript/sdk/src/core/HyperlaneCoreChecker.ts index cb7bb9046..82a26c405 100644 --- a/typescript/sdk/src/core/HyperlaneCoreChecker.ts +++ b/typescript/sdk/src/core/HyperlaneCoreChecker.ts @@ -1,10 +1,12 @@ import { ethers, utils as ethersUtils } from 'ethers'; +import { Ownable__factory } from '@hyperlane-xyz/core'; import { assert, eqAddress, rootLogger } from '@hyperlane-xyz/utils'; import { BytecodeHash } from '../consts/bytecode.js'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker.js'; import { proxyImplementation } from '../deploy/proxy.js'; +import { OwnerViolation, ViolationType } from '../deploy/types.js'; import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { collectValidators, moduleMatchesConfig } from '../ism/utils.js'; @@ -66,6 +68,31 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< return this.checkOwnership(chain, config.owner, config.ownerOverrides); } + async checkHook( + chain: ChainName, + hookName: string, + hookAddress: string, + expectedHookOwner: string, + ): Promise { + const hook = Ownable__factory.connect( + hookAddress, + this.multiProvider.getProvider(chain), + ); + const hookOwner = await hook.owner(); + + if (!eqAddress(hookOwner, expectedHookOwner)) { + const violation: OwnerViolation = { + type: ViolationType.Owner, + chain, + name: hookName, + actual: hookOwner, + expected: expectedHookOwner, + contract: hook, + }; + this.addViolation(violation); + } + } + async checkMailbox(chain: ChainName): Promise { const contracts = this.app.getContracts(chain); const mailbox = contracts.mailbox; @@ -77,9 +104,27 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< )} for ${chain}`, ); - const actualIsmAddress = await mailbox.defaultIsm(); - const config = this.configMap[chain]; + const expectedHookOwner = this.getOwner( + config.owner, + 'fallbackRoutingHook', + config.ownerOverrides, + ); + + await this.checkHook( + chain, + 'defaultHook', + await mailbox.defaultHook(), + expectedHookOwner, + ); + await this.checkHook( + chain, + 'requiredHook', + await mailbox.requiredHook(), + expectedHookOwner, + ); + + const actualIsmAddress = await mailbox.defaultIsm(); const matches = await moduleMatchesConfig( chain, actualIsmAddress, diff --git a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts index 9a9bf4bcc..55f288f08 100644 --- a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts +++ b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts @@ -12,6 +12,7 @@ import { eqAddress, objMap, promiseObjAll, + rootLogger, } from '@hyperlane-xyz/utils'; import { HyperlaneApp } from '../app/HyperlaneApp.js'; @@ -82,6 +83,10 @@ export abstract class HyperlaneAppChecker< } addViolation(violation: CheckerViolation): void { + if (violation.type === ViolationType.BytecodeMismatch) { + rootLogger.warn({ violation }, `Found bytecode mismatch. Ignoring...`); + return; + } this.violations.push(violation); } @@ -208,7 +213,7 @@ export abstract class HyperlaneAppChecker< return bytecode.substring(0, bytecode.length - 90); } - private getOwner( + protected getOwner( owner: Address, contractName: string, ownableOverrides?: Record, diff --git a/typescript/sdk/src/middleware/account/InterchainAccount.ts b/typescript/sdk/src/middleware/account/InterchainAccount.ts index 273efb587..d705fedf3 100644 --- a/typescript/sdk/src/middleware/account/InterchainAccount.ts +++ b/typescript/sdk/src/middleware/account/InterchainAccount.ts @@ -120,6 +120,8 @@ export class InterchainAccount extends RouterApp { .getProvider(destinationChain) .getCode(destinationAccount)) === '0x' ) { + const txOverrides = + this.multiProvider.getTransactionOverrides(destinationChain); await this.multiProvider.handleTx( destinationChain, destinationRouter[ @@ -129,6 +131,7 @@ export class InterchainAccount extends RouterApp { config.owner, originRouterAddress, destinationIsmAddress, + txOverrides, ), ); this.logger.debug(`Interchain account deployed at ${destinationAccount}`); From 5300230c4496a704381f8066452626bc53d3732c Mon Sep 17 00:00:00 2001 From: Danil Nemirovsky Date: Mon, 21 Oct 2024 14:19:40 +0100 Subject: [PATCH 18/26] refactor: Make Sealevel RPC client more functional (#4699) ### Description Make Sealevel RPC client more functional. Move some methods and error mapping into RPC client. ### Backward compatibility Yes ### Testing E2E Ethereum + Sealevel test --------- Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com> --- .../chains/hyperlane-sealevel/src/client.rs | 29 --- .../hyperlane-sealevel/src/interchain_gas.rs | 52 ++-- .../src/interchain_security_module.rs | 29 ++- .../main/chains/hyperlane-sealevel/src/lib.rs | 6 +- .../chains/hyperlane-sealevel/src/mailbox.rs | 118 +++------ .../src/merkle_tree_hook.rs | 10 +- .../hyperlane-sealevel/src/multisig_ism.rs | 32 +-- .../chains/hyperlane-sealevel/src/provider.rs | 27 +- .../main/chains/hyperlane-sealevel/src/rpc.rs | 3 + .../hyperlane-sealevel/src/rpc/client.rs | 242 ++++++++++++++++++ .../chains/hyperlane-sealevel/src/utils.rs | 93 ------- .../src/validator_announce.rs | 22 +- 12 files changed, 350 insertions(+), 313 deletions(-) delete mode 100644 rust/main/chains/hyperlane-sealevel/src/client.rs create mode 100644 rust/main/chains/hyperlane-sealevel/src/rpc.rs create mode 100644 rust/main/chains/hyperlane-sealevel/src/rpc/client.rs delete mode 100644 rust/main/chains/hyperlane-sealevel/src/utils.rs diff --git a/rust/main/chains/hyperlane-sealevel/src/client.rs b/rust/main/chains/hyperlane-sealevel/src/client.rs deleted file mode 100644 index cc41cd0b2..000000000 --- a/rust/main/chains/hyperlane-sealevel/src/client.rs +++ /dev/null @@ -1,29 +0,0 @@ -use solana_client::nonblocking::rpc_client::RpcClient; -use solana_sdk::commitment_config::CommitmentConfig; - -/// Kludge to implement Debug for RpcClient. -pub struct RpcClientWithDebug(RpcClient); - -impl RpcClientWithDebug { - pub fn new(rpc_endpoint: String) -> Self { - Self(RpcClient::new(rpc_endpoint)) - } - - pub fn new_with_commitment(rpc_endpoint: String, commitment: CommitmentConfig) -> Self { - Self(RpcClient::new_with_commitment(rpc_endpoint, commitment)) - } -} - -impl std::fmt::Debug for RpcClientWithDebug { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("RpcClient { ... }") - } -} - -impl std::ops::Deref for RpcClientWithDebug { - type Target = RpcClient; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs index 8c0972c9a..d2f78eb4b 100644 --- a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -16,9 +16,7 @@ use solana_client::{ use std::ops::RangeInclusive; use tracing::{info, instrument}; -use crate::{ - client::RpcClientWithDebug, utils::get_finalized_block_number, ConnectionConf, SealevelProvider, -}; +use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; use derive_new::new; @@ -60,20 +58,14 @@ impl SealevelInterchainGasPaymaster { } async fn determine_igp_program_id( - rpc_client: &RpcClientWithDebug, + rpc_client: &SealevelRpcClient, igp_account_pubkey: &H256, ) -> ChainResult { let account = rpc_client - .get_account_with_commitment( - &Pubkey::from(<[u8; 32]>::from(*igp_account_pubkey)), - CommitmentConfig::finalized(), - ) - .await - .map_err(ChainCommunicationError::from_other)? - .value - .ok_or_else(|| { - ChainCommunicationError::from_other_str("Could not find IGP account for pubkey") - })?; + .get_account_with_finalized_commitment(&Pubkey::from(<[u8; 32]>::from( + *igp_account_pubkey, + ))) + .await?; Ok(account.owner) } } @@ -99,7 +91,7 @@ impl InterchainGasPaymaster for SealevelInterchainGasPaymaster {} /// Struct that retrieves event data for a Sealevel IGP contract #[derive(Debug)] pub struct SealevelInterchainGasPaymasterIndexer { - rpc_client: RpcClientWithDebug, + rpc_client: SealevelRpcClient, igp: SealevelInterchainGasPaymaster, } @@ -118,10 +110,7 @@ impl SealevelInterchainGasPaymasterIndexer { igp_account_locator: ContractLocator<'_>, ) -> ChainResult { // Set the `processed` commitment at rpc level - let rpc_client = RpcClientWithDebug::new_with_commitment( - conf.url.to_string(), - CommitmentConfig::processed(), - ); + let rpc_client = SealevelRpcClient::new(conf.url.to_string()); let igp = SealevelInterchainGasPaymaster::new(conf, &igp_account_locator).await?; Ok(Self { rpc_client, igp }) @@ -169,8 +158,7 @@ impl SealevelInterchainGasPaymasterIndexer { let accounts = self .rpc_client .get_program_accounts_with_config(&self.igp.program_id, config) - .await - .map_err(ChainCommunicationError::from_other)?; + .await?; tracing::debug!(accounts=?accounts, "Fetched program accounts"); @@ -202,13 +190,8 @@ impl SealevelInterchainGasPaymasterIndexer { // Now that we have the valid gas payment PDA pubkey, we can get the full account data. let account = self .rpc_client - .get_account_with_commitment(&valid_payment_pda_pubkey, CommitmentConfig::finalized()) - .await - .map_err(ChainCommunicationError::from_other)? - .value - .ok_or_else(|| { - ChainCommunicationError::from_other_str("Could not find account data") - })?; + .get_account_with_finalized_commitment(&valid_payment_pda_pubkey) + .await?; let gas_payment_account = GasPaymentAccount::fetch(&mut account.data.as_ref()) .map_err(ChainCommunicationError::from_other)? .into_inner(); @@ -274,7 +257,7 @@ impl Indexer for SealevelInterchainGasPaymasterIndexer { #[instrument(level = "debug", err, ret, skip(self))] #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue async fn get_finalized_block_number(&self) -> ChainResult { - get_finalized_block_number(&self.rpc_client).await + self.rpc_client.get_block_height().await } } @@ -285,13 +268,8 @@ impl SequenceAwareIndexer for SealevelInterchainGasPaymast async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { let program_data_account = self .rpc_client - .get_account_with_commitment(&self.igp.data_pda_pubkey, CommitmentConfig::finalized()) - .await - .map_err(ChainCommunicationError::from_other)? - .value - .ok_or_else(|| { - ChainCommunicationError::from_other_str("Could not find account data") - })?; + .get_account_with_finalized_commitment(&self.igp.data_pda_pubkey) + .await?; let program_data = ProgramDataAccount::fetch(&mut program_data_account.data.as_ref()) .map_err(ChainCommunicationError::from_other)? .into_inner(); @@ -299,7 +277,7 @@ impl SequenceAwareIndexer for SealevelInterchainGasPaymast .payment_count .try_into() .map_err(StrOrIntParseError::from)?; - let tip = get_finalized_block_number(&self.rpc_client).await?; + let tip = self.rpc_client.get_block_height().await?; Ok((Some(payment_count), tip)) } } diff --git a/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs b/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs index 0f92432eb..aaf2683fd 100644 --- a/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs +++ b/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs @@ -10,7 +10,7 @@ use hyperlane_core::{ use hyperlane_sealevel_interchain_security_module_interface::InterchainSecurityModuleInstruction; use serializable_account_meta::SimulationReturnData; -use crate::{utils::simulate_instruction, ConnectionConf, RpcClientWithDebug, SealevelProvider}; +use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; /// A reference to an InterchainSecurityModule contract on some Sealevel chain #[derive(Debug)] @@ -32,7 +32,7 @@ impl SealevelInterchainSecurityModule { } } - fn rpc(&self) -> &RpcClientWithDebug { + fn rpc(&self) -> &SealevelRpcClient { self.provider.rpc() } } @@ -64,18 +64,19 @@ impl InterchainSecurityModule for SealevelInterchainSecurityModule { vec![], ); - let module = simulate_instruction::>( - self.rpc(), - self.payer - .as_ref() - .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, - instruction, - ) - .await? - .ok_or_else(|| { - ChainCommunicationError::from_other_str("No return data was returned from the ISM") - })? - .return_data; + let module = self + .rpc() + .simulate_instruction::>( + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await? + .ok_or_else(|| { + ChainCommunicationError::from_other_str("No return data was returned from the ISM") + })? + .return_data; if let Some(module_type) = ModuleType::from_u32(module) { Ok(module_type) diff --git a/rust/main/chains/hyperlane-sealevel/src/lib.rs b/rust/main/chains/hyperlane-sealevel/src/lib.rs index 8cd8830f5..04e2218c6 100644 --- a/rust/main/chains/hyperlane-sealevel/src/lib.rs +++ b/rust/main/chains/hyperlane-sealevel/src/lib.rs @@ -5,12 +5,12 @@ #![deny(warnings)] pub use crate::multisig_ism::*; -pub(crate) use client::RpcClientWithDebug; pub use interchain_gas::*; pub use interchain_security_module::*; pub use mailbox::*; pub use merkle_tree_hook::*; pub use provider::*; +pub(crate) use rpc::SealevelRpcClient; pub use solana_sdk::signer::keypair::Keypair; pub use trait_builder::*; pub use validator_announce::*; @@ -22,8 +22,6 @@ mod mailbox; mod merkle_tree_hook; mod multisig_ism; mod provider; +mod rpc; mod trait_builder; -mod utils; - -mod client; mod validator_announce; diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index f101435c2..952599c42 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -8,11 +8,12 @@ use jsonrpc_core::futures_util::TryFutureExt; use tracing::{debug, info, instrument, warn}; use hyperlane_core::{ - accumulator::incremental::IncrementalMerkle, BatchItem, ChainCommunicationError, ChainResult, - Checkpoint, ContractLocator, Decode as _, Encode as _, FixedPointNumber, HyperlaneAbi, - HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, - Indexed, Indexer, KnownHyperlaneDomain, LogMeta, Mailbox, MerkleTreeHook, SequenceAwareIndexer, - TxCostEstimate, TxOutcome, H256, H512, U256, + accumulator::incremental::IncrementalMerkle, BatchItem, ChainCommunicationError, + ChainCommunicationError::ContractError, ChainResult, Checkpoint, ContractLocator, Decode as _, + Encode as _, FixedPointNumber, HyperlaneAbi, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, KnownHyperlaneDomain, + LogMeta, Mailbox, MerkleTreeHook, SequenceAwareIndexer, TxCostEstimate, TxOutcome, H256, H512, + U256, }; use hyperlane_sealevel_interchain_security_module_interface::{ InterchainSecurityModuleInstruction, VerifyInstruction, @@ -54,11 +55,7 @@ use solana_transaction_status::{ UiTransaction, UiTransactionReturnData, UiTransactionStatusMeta, }; -use crate::RpcClientWithDebug; -use crate::{ - utils::{get_account_metas, get_finalized_block_number, simulate_instruction}, - ConnectionConf, SealevelProvider, -}; +use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111"; const SPL_NOOP: &str = "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"; @@ -128,7 +125,7 @@ impl SealevelMailbox { self.outbox } - pub fn rpc(&self) -> &RpcClientWithDebug { + pub fn rpc(&self) -> &SealevelRpcClient { self.provider.rpc() } @@ -140,14 +137,14 @@ impl SealevelMailbox { &self, instruction: Instruction, ) -> ChainResult> { - simulate_instruction( - &self.rpc(), - self.payer - .as_ref() - .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, - instruction, - ) - .await + self.rpc() + .simulate_instruction( + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await } /// Simulates an Instruction that will return a list of AccountMetas. @@ -155,14 +152,14 @@ impl SealevelMailbox { &self, instruction: Instruction, ) -> ChainResult> { - get_account_metas( - &self.rpc(), - self.payer - .as_ref() - .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, - instruction, - ) - .await + self.rpc() + .get_account_metas( + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await } /// Gets the recipient ISM given a recipient program id and the ISM getter account metas. @@ -293,7 +290,6 @@ impl SealevelMailbox { .rpc() .send_and_confirm_transaction(transaction) .await - .map_err(ChainCommunicationError::from_other) } } @@ -343,13 +339,10 @@ impl SealevelMailbox { ); let recent_blockhash = if transaction.uses_durable_nonce() { - let (recent_blockhash, ..) = self - .provider + self.provider .rpc() .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) - .await - .map_err(ChainCommunicationError::from_other)?; - recent_blockhash + .await? } else { *transaction.get_recent_blockhash() }; @@ -359,8 +352,7 @@ impl SealevelMailbox { .provider .rpc() .get_signature_statuses(&[*signature]) - .await - .map_err(ChainCommunicationError::from_other)?; + .await?; let signature_status = signature_statuses.value.first().cloned().flatten(); match signature_status { Some(_) => return Ok(*signature), @@ -368,9 +360,8 @@ impl SealevelMailbox { if !self .provider .rpc() - .is_blockhash_valid(&recent_blockhash, CommitmentConfig::processed()) - .await - .map_err(ChainCommunicationError::from_other)? + .is_blockhash_valid(&recent_blockhash) + .await? { // Block hash is not found by some reason break 'sending; @@ -439,23 +430,15 @@ impl Mailbox for SealevelMailbox { let account = self .rpc() - .get_account_with_commitment( - &processed_message_account_key, - CommitmentConfig::finalized(), - ) - .await - .map_err(ChainCommunicationError::from_other)?; + .get_possible_account_with_finalized_commitment(&processed_message_account_key) + .await?; - Ok(account.value.is_some()) + Ok(account.is_some()) } #[instrument(err, ret, skip(self))] async fn default_ism(&self) -> ChainResult { - let inbox_account = self - .rpc() - .get_account(&self.inbox.0) - .await - .map_err(ChainCommunicationError::from_other)?; + let inbox_account = self.rpc().get_account(&self.inbox.0).await?; let inbox = InboxAccount::fetch(&mut inbox_account.data.as_ref()) .map_err(ChainCommunicationError::from_other)? .into_inner(); @@ -591,11 +574,10 @@ impl Mailbox for SealevelMailbox { accounts, }; instructions.push(inbox_instruction); - let (recent_blockhash, _) = self + let recent_blockhash = self .rpc() .get_latest_blockhash_with_commitment(commitment) - .await - .map_err(ChainCommunicationError::from_other)?; + .await?; let txn = Transaction::new_signed_with_payer( &instructions, @@ -615,7 +597,6 @@ impl Mailbox for SealevelMailbox { .confirm_transaction_with_commitment(&signature, commitment) .await .map_err(|err| warn!("Failed to confirm inbox process transaction: {}", err)) - .map(|ctx| ctx.value) .unwrap_or(false); let txid = signature.into(); @@ -664,20 +645,12 @@ impl SealevelMailboxIndexer { }) } - fn rpc(&self) -> &RpcClientWithDebug { + fn rpc(&self) -> &SealevelRpcClient { &self.mailbox.rpc() } async fn get_finalized_block_number(&self) -> ChainResult { - let height = self - .rpc() - .get_block_height() - .await - .map_err(ChainCommunicationError::from_other)? - .try_into() - // FIXME solana block height is u64... - .expect("sealevel block height exceeds u32::MAX"); - Ok(height) + self.rpc().get_block_height().await } async fn get_message_with_nonce( @@ -718,8 +691,7 @@ impl SealevelMailboxIndexer { let accounts = self .rpc() .get_program_accounts_with_config(&self.mailbox.program_id, config) - .await - .map_err(ChainCommunicationError::from_other)?; + .await?; // Now loop through matching accounts and find the one with a valid account pubkey // that proves it's an actual message storage PDA. @@ -752,16 +724,8 @@ impl SealevelMailboxIndexer { // Now that we have the valid message storage PDA pubkey, we can get the full account data. let account = self .rpc() - .get_account_with_commitment( - &valid_message_storage_pda_pubkey, - CommitmentConfig::finalized(), - ) - .await - .map_err(ChainCommunicationError::from_other)? - .value - .ok_or_else(|| { - ChainCommunicationError::from_other_str("Could not find account data") - })?; + .get_account_with_finalized_commitment(&valid_message_storage_pda_pubkey) + .await?; let dispatched_message_account = DispatchedMessageAccount::fetch(&mut account.data.as_ref()) .map_err(ChainCommunicationError::from_other)? @@ -816,7 +780,7 @@ impl Indexer for SealevelMailboxIndexer { } async fn get_finalized_block_number(&self) -> ChainResult { - get_finalized_block_number(&self.rpc()).await + self.get_finalized_block_number().await } } diff --git a/rust/main/chains/hyperlane-sealevel/src/merkle_tree_hook.rs b/rust/main/chains/hyperlane-sealevel/src/merkle_tree_hook.rs index 3778627b2..947f7e70a 100644 --- a/rust/main/chains/hyperlane-sealevel/src/merkle_tree_hook.rs +++ b/rust/main/chains/hyperlane-sealevel/src/merkle_tree_hook.rs @@ -8,7 +8,6 @@ use hyperlane_core::{ MerkleTreeInsertion, SequenceAwareIndexer, }; use hyperlane_sealevel_mailbox::accounts::OutboxAccount; -use solana_sdk::commitment_config::CommitmentConfig; use tracing::instrument; use crate::{SealevelMailbox, SealevelMailboxIndexer}; @@ -25,13 +24,8 @@ impl MerkleTreeHook for SealevelMailbox { let outbox_account = self .rpc() - .get_account_with_commitment(&self.outbox.0, CommitmentConfig::finalized()) - .await - .map_err(ChainCommunicationError::from_other)? - .value - .ok_or_else(|| { - ChainCommunicationError::from_other_str("Could not find account data") - })?; + .get_account_with_finalized_commitment(&self.outbox.0) + .await?; let outbox = OutboxAccount::fetch(&mut outbox_account.data.as_ref()) .map_err(ChainCommunicationError::from_other)? .into_inner(); diff --git a/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs b/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs index 794e19c14..a3cdb1273 100644 --- a/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs +++ b/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs @@ -1,9 +1,9 @@ use async_trait::async_trait; - use hyperlane_core::{ ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, MultisigIsm, RawHyperlaneMessage, H256, }; +use hyperlane_sealevel_multisig_ism_message_id::instruction::ValidatorsAndThreshold; use serializable_account_meta::SimulationReturnData; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -11,12 +11,8 @@ use solana_sdk::{ signature::Keypair, }; -use crate::{ - utils::{get_account_metas, simulate_instruction}, - ConnectionConf, RpcClientWithDebug, SealevelProvider, -}; +use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; -use hyperlane_sealevel_multisig_ism_message_id::instruction::ValidatorsAndThreshold; use multisig_ism::interface::{ MultisigIsmInstruction, VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS, }; @@ -44,7 +40,7 @@ impl SealevelMultisigIsm { } } - fn rpc(&self) -> &RpcClientWithDebug { + fn rpc(&self) -> &SealevelRpcClient { self.provider.rpc() } } @@ -86,9 +82,9 @@ impl MultisigIsm for SealevelMultisigIsm { account_metas, ); - let validators_and_threshold = - simulate_instruction::>( - self.rpc(), + let validators_and_threshold = self + .rpc() + .simulate_instruction::>( self.payer .as_ref() .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, @@ -135,13 +131,13 @@ impl SealevelMultisigIsm { vec![AccountMeta::new_readonly(account_metas_pda_key, false)], ); - get_account_metas( - self.rpc(), - self.payer - .as_ref() - .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, - instruction, - ) - .await + self.rpc() + .get_account_metas( + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await } } diff --git a/rust/main/chains/hyperlane-sealevel/src/provider.rs b/rust/main/chains/hyperlane-sealevel/src/provider.rs index 42b266550..b292d9594 100644 --- a/rust/main/chains/hyperlane-sealevel/src/provider.rs +++ b/rust/main/chains/hyperlane-sealevel/src/provider.rs @@ -6,44 +6,30 @@ use hyperlane_core::{ BlockInfo, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, TxnInfo, H256, U256, }; -use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; +use solana_sdk::pubkey::Pubkey; -use crate::{client::RpcClientWithDebug, error::HyperlaneSealevelError, ConnectionConf}; +use crate::{error::HyperlaneSealevelError, ConnectionConf, SealevelRpcClient}; /// A wrapper around a Sealevel provider to get generic blockchain information. #[derive(Debug)] pub struct SealevelProvider { domain: HyperlaneDomain, - rpc_client: Arc, + rpc_client: Arc, } impl SealevelProvider { /// Create a new Sealevel provider. pub fn new(domain: HyperlaneDomain, conf: &ConnectionConf) -> Self { // Set the `processed` commitment at rpc level - let rpc_client = Arc::new(RpcClientWithDebug::new_with_commitment( - conf.url.to_string(), - CommitmentConfig::processed(), - )); + let rpc_client = Arc::new(SealevelRpcClient::new(conf.url.to_string())); SealevelProvider { domain, rpc_client } } /// Get an rpc client - pub fn rpc(&self) -> &RpcClientWithDebug { + pub fn rpc(&self) -> &SealevelRpcClient { &self.rpc_client } - - /// Get the balance of an address - pub async fn get_balance(&self, address: String) -> ChainResult { - let pubkey = Pubkey::from_str(&address).map_err(Into::::into)?; - let balance = self - .rpc_client - .get_balance(&pubkey) - .await - .map_err(Into::::into)?; - Ok(balance.into()) - } } impl HyperlaneChain for SealevelProvider { @@ -75,7 +61,8 @@ impl HyperlaneProvider for SealevelProvider { } async fn get_balance(&self, address: String) -> ChainResult { - self.get_balance(address).await + let pubkey = Pubkey::from_str(&address).map_err(Into::::into)?; + self.rpc_client.get_balance(&pubkey).await } async fn get_chain_metrics(&self) -> ChainResult> { diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc.rs b/rust/main/chains/hyperlane-sealevel/src/rpc.rs new file mode 100644 index 000000000..1c82b77c0 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/rpc.rs @@ -0,0 +1,3 @@ +pub use client::SealevelRpcClient; + +mod client; diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs new file mode 100644 index 000000000..77f21ee1f --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs @@ -0,0 +1,242 @@ +use base64::Engine; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{ChainCommunicationError, ChainResult, U256}; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_client::{ + nonblocking::rpc_client::RpcClient, rpc_config::RpcProgramAccountsConfig, + rpc_response::Response, +}; +use solana_sdk::{ + account::Account, + commitment_config::CommitmentConfig, + hash::Hash, + instruction::{AccountMeta, Instruction}, + message::Message, + pubkey::Pubkey, + signature::{Keypair, Signature, Signer}, + transaction::Transaction, +}; +use solana_transaction_status::{TransactionStatus, UiReturnDataEncoding, UiTransactionReturnData}; + +use crate::error::HyperlaneSealevelError; + +pub struct SealevelRpcClient(RpcClient); + +impl SealevelRpcClient { + pub fn new(rpc_endpoint: String) -> Self { + Self(RpcClient::new_with_commitment( + rpc_endpoint, + CommitmentConfig::processed(), + )) + } + + pub async fn confirm_transaction_with_commitment( + &self, + signature: &Signature, + commitment: CommitmentConfig, + ) -> ChainResult { + self.0 + .confirm_transaction_with_commitment(signature, commitment) + .await + .map(|ctx| ctx.value) + .map_err(HyperlaneSealevelError::ClientError) + .map_err(Into::into) + } + + pub async fn get_account(&self, pubkey: &Pubkey) -> ChainResult { + self.0 + .get_account(pubkey) + .await + .map_err(ChainCommunicationError::from_other) + } + + /// Simulates an Instruction that will return a list of AccountMetas. + pub async fn get_account_metas( + &self, + payer: &Keypair, + instruction: Instruction, + ) -> ChainResult> { + // If there's no data at all, default to an empty vec. + let account_metas = self + .simulate_instruction::>>( + payer, + instruction, + ) + .await? + .map(|serializable_account_metas| { + serializable_account_metas + .return_data + .into_iter() + .map(|serializable_account_meta| serializable_account_meta.into()) + .collect() + }) + .unwrap_or_else(Vec::new); + + Ok(account_metas) + } + + pub async fn get_account_with_finalized_commitment( + &self, + pubkey: &Pubkey, + ) -> ChainResult { + self.get_possible_account_with_finalized_commitment(pubkey) + .await? + .ok_or_else(|| ChainCommunicationError::from_other_str("Could not find account data")) + } + + pub async fn get_possible_account_with_finalized_commitment( + &self, + pubkey: &Pubkey, + ) -> ChainResult> { + let account = self + .0 + .get_account_with_commitment(pubkey, CommitmentConfig::finalized()) + .await + .map_err(ChainCommunicationError::from_other)? + .value; + Ok(account) + } + + pub async fn get_block_height(&self) -> ChainResult { + let height = self + .0 + .get_block_height_with_commitment(CommitmentConfig::finalized()) + .await + .map_err(ChainCommunicationError::from_other)? + .try_into() + // FIXME solana block height is u64... + .expect("sealevel block height exceeds u32::MAX"); + Ok(height) + } + + pub async fn get_multiple_accounts_with_finalized_commitment( + &self, + pubkeys: &[Pubkey], + ) -> ChainResult>> { + let accounts = self + .0 + .get_multiple_accounts_with_commitment(pubkeys, CommitmentConfig::finalized()) + .await + .map_err(ChainCommunicationError::from_other)? + .value; + + Ok(accounts) + } + + pub async fn get_latest_blockhash_with_commitment( + &self, + commitment: CommitmentConfig, + ) -> ChainResult { + self.0 + .get_latest_blockhash_with_commitment(commitment) + .await + .map_err(ChainCommunicationError::from_other) + .map(|(blockhash, _)| blockhash) + } + + pub async fn get_program_accounts_with_config( + &self, + pubkey: &Pubkey, + config: RpcProgramAccountsConfig, + ) -> ChainResult> { + self.0 + .get_program_accounts_with_config(pubkey, config) + .await + .map_err(ChainCommunicationError::from_other) + } + + pub async fn get_signature_statuses( + &self, + signatures: &[Signature], + ) -> ChainResult>>> { + self.0 + .get_signature_statuses(signatures) + .await + .map_err(ChainCommunicationError::from_other) + } + + pub async fn get_balance(&self, pubkey: &Pubkey) -> ChainResult { + let balance = self + .0 + .get_balance(pubkey) + .await + .map_err(Into::::into) + .map_err(ChainCommunicationError::from)?; + + Ok(balance.into()) + } + + pub async fn is_blockhash_valid(&self, hash: &Hash) -> ChainResult { + self.0 + .is_blockhash_valid(hash, CommitmentConfig::processed()) + .await + .map_err(ChainCommunicationError::from_other) + } + + pub async fn send_and_confirm_transaction( + &self, + transaction: &Transaction, + ) -> ChainResult { + self.0 + .send_and_confirm_transaction(transaction) + .await + .map_err(ChainCommunicationError::from_other) + } + + /// Simulates an instruction, and attempts to deserialize it into a T. + /// If no return data at all was returned, returns Ok(None). + /// If some return data was returned but deserialization was unsuccessful, + /// an Err is returned. + pub async fn simulate_instruction( + &self, + payer: &Keypair, + instruction: Instruction, + ) -> ChainResult> { + let commitment = CommitmentConfig::finalized(); + let recent_blockhash = self + .get_latest_blockhash_with_commitment(commitment) + .await?; + let transaction = Transaction::new_unsigned(Message::new_with_blockhash( + &[instruction], + Some(&payer.pubkey()), + &recent_blockhash, + )); + let return_data = self.simulate_transaction(&transaction).await?; + + if let Some(return_data) = return_data { + let bytes = match return_data.data.1 { + UiReturnDataEncoding::Base64 => base64::engine::general_purpose::STANDARD + .decode(return_data.data.0) + .map_err(ChainCommunicationError::from_other)?, + }; + + let decoded_data = + T::try_from_slice(bytes.as_slice()).map_err(ChainCommunicationError::from_other)?; + + return Ok(Some(decoded_data)); + } + + Ok(None) + } + + async fn simulate_transaction( + &self, + transaction: &Transaction, + ) -> ChainResult> { + let return_data = self + .0 + .simulate_transaction(transaction) + .await + .map_err(ChainCommunicationError::from_other)? + .value + .return_data; + + Ok(return_data) + } +} + +impl std::fmt::Debug for SealevelRpcClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("RpcClient { ... }") + } +} diff --git a/rust/main/chains/hyperlane-sealevel/src/utils.rs b/rust/main/chains/hyperlane-sealevel/src/utils.rs deleted file mode 100644 index 56bec9e51..000000000 --- a/rust/main/chains/hyperlane-sealevel/src/utils.rs +++ /dev/null @@ -1,93 +0,0 @@ -use base64::Engine; -use borsh::{BorshDeserialize, BorshSerialize}; -use hyperlane_core::{ChainCommunicationError, ChainResult}; - -use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; -use solana_client::nonblocking::rpc_client::RpcClient; -use solana_sdk::{ - commitment_config::CommitmentConfig, - instruction::{AccountMeta, Instruction}, - message::Message, - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solana_transaction_status::UiReturnDataEncoding; - -use crate::client::RpcClientWithDebug; - -/// Simulates an instruction, and attempts to deserialize it into a T. -/// If no return data at all was returned, returns Ok(None). -/// If some return data was returned but deserialization was unsuccessful, -/// an Err is returned. -pub async fn simulate_instruction( - rpc_client: &RpcClient, - payer: &Keypair, - instruction: Instruction, -) -> ChainResult> { - let commitment = CommitmentConfig::finalized(); - let (recent_blockhash, _) = rpc_client - .get_latest_blockhash_with_commitment(commitment) - .await - .map_err(ChainCommunicationError::from_other)?; - let return_data = rpc_client - .simulate_transaction(&Transaction::new_unsigned(Message::new_with_blockhash( - &[instruction], - Some(&payer.pubkey()), - &recent_blockhash, - ))) - .await - .map_err(ChainCommunicationError::from_other)? - .value - .return_data; - - if let Some(return_data) = return_data { - let bytes = match return_data.data.1 { - UiReturnDataEncoding::Base64 => base64::engine::general_purpose::STANDARD - .decode(return_data.data.0) - .map_err(ChainCommunicationError::from_other)?, - }; - - let decoded_data = - T::try_from_slice(bytes.as_slice()).map_err(ChainCommunicationError::from_other)?; - - return Ok(Some(decoded_data)); - } - - Ok(None) -} - -/// Simulates an Instruction that will return a list of AccountMetas. -pub async fn get_account_metas( - rpc_client: &RpcClient, - payer: &Keypair, - instruction: Instruction, -) -> ChainResult> { - // If there's no data at all, default to an empty vec. - let account_metas = simulate_instruction::>>( - rpc_client, - payer, - instruction, - ) - .await? - .map(|serializable_account_metas| { - serializable_account_metas - .return_data - .into_iter() - .map(|serializable_account_meta| serializable_account_meta.into()) - .collect() - }) - .unwrap_or_else(Vec::new); - - Ok(account_metas) -} - -pub async fn get_finalized_block_number(rpc_client: &RpcClientWithDebug) -> ChainResult { - let height = rpc_client - .get_block_height() - .await - .map_err(ChainCommunicationError::from_other)? - .try_into() - // FIXME solana block height is u64... - .expect("sealevel block height exceeds u32::MAX"); - Ok(height) -} diff --git a/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs b/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs index 52b19495a..3edfa0d06 100644 --- a/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs +++ b/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs @@ -1,17 +1,15 @@ use async_trait::async_trait; -use tracing::{info, instrument, warn}; - use hyperlane_core::{ - Announcement, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, - HyperlaneContract, HyperlaneDomain, SignedType, TxOutcome, ValidatorAnnounce, H160, H256, H512, - U256, + Announcement, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + SignedType, TxOutcome, ValidatorAnnounce, H160, H256, H512, U256, }; -use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; - -use crate::{ConnectionConf, RpcClientWithDebug, SealevelProvider}; use hyperlane_sealevel_validator_announce::{ accounts::ValidatorStorageLocationsAccount, validator_storage_locations_pda_seeds, }; +use solana_sdk::pubkey::Pubkey; +use tracing::{info, instrument, warn}; + +use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; /// A reference to a ValidatorAnnounce contract on some Sealevel chain #[derive(Debug)] @@ -33,7 +31,7 @@ impl SealevelValidatorAnnounce { } } - fn rpc(&self) -> &RpcClientWithDebug { + fn rpc(&self) -> &SealevelRpcClient { self.provider.rpc() } } @@ -79,10 +77,8 @@ impl ValidatorAnnounce for SealevelValidatorAnnounce { // If an account doesn't exist, it will be returned as None. let accounts = self .rpc() - .get_multiple_accounts_with_commitment(&account_pubkeys, CommitmentConfig::finalized()) - .await - .map_err(ChainCommunicationError::from_other)? - .value; + .get_multiple_accounts_with_finalized_commitment(&account_pubkeys) + .await?; // Parse the storage locations from each account. // If a validator's account doesn't exist, its storage locations will From 7bfc7ec81c21ba64c3462445113fa58946bd28e2 Mon Sep 17 00:00:00 2001 From: Lee <6251863+ltyu@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:40:06 -0400 Subject: [PATCH 19/26] chore: remove old e2e cli tests (#4711) ### Description Removes the tests from workflow. We now use ts e2e tests ### Related issues - Fixes #4417 --- .github/workflows/test.yml | 34 ---- typescript/cli/ci-advanced-test.sh | 270 ----------------------------- 2 files changed, 304 deletions(-) delete mode 100755 typescript/cli/ci-advanced-test.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1b7d7694..b37075002 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -234,40 +234,6 @@ jobs: RUST_BACKTRACE: 'full' SEALEVEL_ENABLED: ${{ steps.check-rust-changes.outputs.rust_changes }} - cli-advanced-e2e: - runs-on: ubuntu-latest - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main' || github.base_ref == 'cli-2.0') || github.event_name == 'merge_group' - needs: [yarn-install] - strategy: - matrix: - include: - - test-type: preset_hook_enabled - - test-type: configure_hook_enabled - - test-type: pi_with_core_chain - steps: - - uses: actions/setup-node@v4 - with: - node-version: 18 - - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - submodules: recursive - - - name: foundry-install - uses: foundry-rs/foundry-toolchain@v1 - - - name: yarn-build - uses: ./.github/actions/yarn-build-with-cache - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - - - name: Checkout registry - uses: ./.github/actions/checkout-registry - - - name: cli e2e tests - run: ./typescript/cli/ci-advanced-test.sh ${{ matrix.test-type }} - env-test: runs-on: ubuntu-latest env: diff --git a/typescript/cli/ci-advanced-test.sh b/typescript/cli/ci-advanced-test.sh deleted file mode 100755 index 5fd16676f..000000000 --- a/typescript/cli/ci-advanced-test.sh +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env bash - -_main() { - export LOG_LEVEL=DEBUG - - # set script location as repo root - cd "$(dirname "$0")/../.." - - TEST_TYPE_PRESET_HOOK="preset_hook_enabled" - TEST_TYPE_CONFIGURED_HOOK="configure_hook_enabled" - TEST_TYPE_PI_CORE="pi_with_core_chain" - - # set the first arg to 'configured_hook' to set the hook config as part of core deployment - # motivation is to test both the bare bone deployment (included in the docs) and the deployment - # with the routing over igp hook (which is closer to production deployment) - TEST_TYPE=$1 - if [ -z "$TEST_TYPE" ]; then - echo "Usage: ci-advanced-test.sh <$TEST_TYPE_PRESET_HOOK | $TEST_TYPE_CONFIGURED_HOOK | $TEST_TYPE_PI_CORE>" - exit 1 - fi - - prepare_environment_vars; - - prepare_anvil; - - DEPLOYER=$(cast rpc eth_accounts | jq -r '.[0]'); - - # TODO: fix `resetFork` after a dry-run. Related: https://github.com/foundry-rs/foundry/pull/8768 - # run_hyperlane_deploy_core_dry_run; - # run_hyperlane_deploy_warp_dry_run; - # reset_anvil; - - run_hyperlane_deploy_core; - run_hyperlane_deploy_warp; - run_hyperlane_send_message; - - kill_anvil; - - echo "Done"; -} - -prepare_environment_vars() { - ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 - CHAIN1=anvil1 - CHAIN2=anvil2 - EXAMPLES_PATH=./examples - TEST_CONFIGS_PATH=./test-configs - CLI_PATH=./typescript/cli - REGISTRY_PATH="$TEST_CONFIGS_PATH/anvil" - CORE_ISM_PATH="$EXAMPLES_PATH/ism.yaml" - WARP_DEPLOY_CONFIG_PATH="$EXAMPLES_PATH/warp-route-deployment.yaml" - DEPLOY_ERC20_PATH=./src/tests/deployTestErc20.ts - - # use different chain names and config for pi<>core test - if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then - CHAIN2=ethereum - REGISTRY_PATH="$TEST_CONFIGS_PATH/fork" - CORE_ISM_PATH="$REGISTRY_PATH/ism.yaml" - WARP_DEPLOY_CONFIG_PATH="$REGISTRY_PATH/warp-route-deployment.yaml" - fi - - CHAIN1_CAPS=$(echo "${CHAIN1}" | tr '[:lower:]' '[:upper:]') - CHAIN2_CAPS=$(echo "${CHAIN2}" | tr '[:lower:]' '[:upper:]') - - HOOK_FLAG=false - if [ "$TEST_TYPE" == $TEST_TYPE_CONFIGURED_HOOK ]; then - HOOK_FLAG=true - fi -} - -prepare_anvil() { - - CHAIN1_PORT=8545 - CHAIN2_PORT=8555 - - # Optional cleanup for previous runs, useful when running locally - pkill -f anvil - rm -rf /tmp/${CHAIN1}* - rm -rf /tmp/${CHAIN2}* - rm -rf /tmp/relayer - rm -f $CLI_PATH/$TEST_CONFIGS_PATH/*/chains/*/addresses.yaml - rm -rf $CLI_PATH/$TEST_CONFIGS_PATH/*/deployments - - if [[ $OSTYPE == 'darwin'* ]]; then - # kill child processes on exit, but only locally because - # otherwise it causes the script exit code to be non-zero - trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT - fi - - # Setup directories for anvil chains - for CHAIN in ${CHAIN1} ${CHAIN2} - do - mkdir -p /tmp/$CHAIN /tmp/$CHAIN/state /tmp/$CHAIN/validator /tmp/relayer - chmod -R 777 /tmp/relayer /tmp/$CHAIN - done - - # run the PI chain - anvil --chain-id 31337 -p ${CHAIN1_PORT} --state /tmp/${CHAIN1}/state --gas-price 1 > /dev/null & - sleep 1 - - # use different chain names for pi<>core test - if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then - # Fetch the RPC of chain to fork - cd typescript/infra - RPC_URL=$(LOG_LEVEL=error yarn tsx scripts/print-chain-metadatas.ts -e mainnet3 | jq -r ".${CHAIN2}.rpcUrls[0].http") - cd ../../ - - # run the fork chain - anvil -p ${CHAIN2_PORT} --state /tmp/${CHAIN2}/state --gas-price 1 --fork-url $RPC_URL --fork-retry-backoff 3 --compute-units-per-second 200 > /dev/null & - - # wait for fork to be ready - while ! cast bn --rpc-url http://127.0.0.1:${CHAIN2_PORT} &> /dev/null; do - sleep 1 - done - else - # run a second PI chain - anvil --chain-id 31338 -p ${CHAIN2_PORT} --state /tmp/${CHAIN2}/state --gas-price 1 > /dev/null & - sleep 1 - fi - - set -e -} - -reset_anvil() { - prepare_anvil -} - -kill_anvil() { - pkill -f anvil -} - -run_hyperlane_deploy_core_dry_run() { - if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then - return; - fi - - update_deployer_balance; - - echo -e "\nDry-running contract deployments to Alfajores" - yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ - --dry-run alfajores \ - --registry ${TEST_CONFIGS_PATH}/dry-run \ - --overrides " " \ - --config ${EXAMPLES_PATH}/core-config.yaml \ - --from-address 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \ - --yes - - check_deployer_balance; -} - -run_hyperlane_deploy_warp_dry_run() { - if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then - return; - fi - - update_deployer_balance; - - echo -e "\nDry-running warp route deployments to Alfajores" - yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \ - --dry-run alfajores \ - --overrides ${TEST_CONFIGS_PATH}/dry-run \ - --config ${TEST_CONFIGS_PATH}/dry-run/warp-route-deployment.yaml \ - --from-address 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \ - --yes - - check_deployer_balance; -} - -run_hyperlane_deploy_core() { - update_deployer_balance; - - echo -e "\nDeploying contracts to ${CHAIN1}" - yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ - --registry $REGISTRY_PATH \ - --overrides " " \ - --config ${EXAMPLES_PATH}/core-config.yaml \ - --chain $CHAIN1 \ - --key $ANVIL_KEY \ - --yes - - echo -e "\nDeploying contracts to ${CHAIN2}" - yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ - --registry $REGISTRY_PATH \ - --overrides " " \ - --config ${EXAMPLES_PATH}/core-config.yaml \ - --chain $CHAIN2 \ - --key $ANVIL_KEY \ - --yes - - check_deployer_balance; -} - -run_hyperlane_deploy_warp() { - update_deployer_balance; - - echo -e "\nDeploying hypNative warp route" - yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \ - --registry $REGISTRY_PATH \ - --overrides " " \ - --config $WARP_DEPLOY_CONFIG_PATH \ - --key $ANVIL_KEY \ - --yes - - yarn workspace @hyperlane-xyz/cli run tsx $DEPLOY_ERC20_PATH \ - http://127.0.0.1:$CHAIN1_PORT \ - $CHAIN1 $CHAIN2 $ANVIL_KEY \ - /tmp/warp-collateral-deployment.json \ - - echo "Deploying hypCollateral warp route" - yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \ - --registry $REGISTRY_PATH \ - --overrides " " \ - --config /tmp/warp-collateral-deployment.json \ - --key $ANVIL_KEY \ - --yes - - check_deployer_balance; -} - -run_hyperlane_send_message() { - update_deployer_balance; - - echo -e "\nSending test message" - yarn workspace @hyperlane-xyz/cli run hyperlane send message \ - --registry $REGISTRY_PATH \ - --overrides " " \ - --origin ${CHAIN1} \ - --destination ${CHAIN2} \ - --body "Howdy!" \ - --quick \ - --key $ANVIL_KEY \ - | tee /tmp/message1 - - check_deployer_balance; - - MESSAGE1_ID=`cat /tmp/message1 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'` - echo "Message 1 ID: $MESSAGE1_ID" - - WARP_CONFIG_FILE="$REGISTRY_PATH/deployments/warp_routes/FAKE/${CHAIN1}-${CHAIN2}-config.yaml" - - echo -e "\nSending test warp transfer" - yarn workspace @hyperlane-xyz/cli run hyperlane warp send \ - --registry $REGISTRY_PATH \ - --overrides " " \ - --origin ${CHAIN1} \ - --destination ${CHAIN2} \ - --warp ${WARP_CONFIG_FILE} \ - --quick \ - --relay \ - --key $ANVIL_KEY \ - | tee /tmp/message2 - - MESSAGE2_ID=`cat /tmp/message2 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'` - echo "Message 2 ID: $MESSAGE2_ID" -} - -update_deployer_balance() { - OLD_BALANCE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT}); -} - -check_deployer_balance() { - NEW_BALANCE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT}) - GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT}) - GAS_USED=$(bc <<< "($OLD_BALANCE - $NEW_BALANCE) / $GAS_PRICE") - echo "Gas used: $GAS_USED" -} - -_main "$@"; - -exit; From 32d0a67c2130f5f893bd824ccfb05007418cf191 Mon Sep 17 00:00:00 2001 From: xeno097 Date: Mon, 21 Oct 2024 18:37:00 -0400 Subject: [PATCH 20/26] feat: cli warp route checker (#4667) ### Description This PR implements the `warp check` command to compare on-chain warp deployments with provided configuration files ### Drive-by changes - updated the `inputFileCommandOption` to be a function for defining cli input file args - defined the `DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH` to avoid hardcoding the `./configs/warp-route-deployment.yaml` file path - implemented the `logCommandHeader` function to format the command header and avoid always having to manually log the `---` separator in command outputs - implements the `getWarpCoreConfigOrExit` to get the warp core configuration from either the registry or a user-defined path and exit early if no input value is provided - Updated the `IsmConfigSchema`s to include the `BaseIsmConfigSchema` because when parsing config files the address field was not included in the parsed object as it wasn't defined on the type Example output ![image](https://github.com/user-attachments/assets/07821a13-d2e2-4b73-b493-9a2c2829a7b3) ![image](https://github.com/user-attachments/assets/768d724f-c96e-4ff5-9c4d-332560c57626) ![image](https://github.com/user-attachments/assets/f92df7c5-acac-4ff7-974b-0334e4a221ab) ### Related issues - Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4666 ### Backward compatibility - Yes ### Testing - Manual --- .changeset/grumpy-ears-relate.md | 6 + typescript/cli/src/check/warp.ts | 41 ++++++ typescript/cli/src/commands/config.ts | 8 +- typescript/cli/src/commands/options.ts | 26 +++- typescript/cli/src/commands/warp.ts | 186 ++++++++++--------------- typescript/cli/src/logger.ts | 4 + typescript/cli/src/read/warp.ts | 117 ++++++++++++++++ typescript/cli/src/utils/input.ts | 35 ++++- typescript/cli/src/utils/output.ts | 56 ++++++++ typescript/sdk/src/index.ts | 2 +- typescript/utils/src/index.ts | 2 + typescript/utils/src/objects.test.ts | 136 +++++++++++++++++- typescript/utils/src/objects.ts | 101 ++++++++++++++ 13 files changed, 596 insertions(+), 124 deletions(-) create mode 100644 .changeset/grumpy-ears-relate.md create mode 100644 typescript/cli/src/check/warp.ts create mode 100644 typescript/cli/src/read/warp.ts create mode 100644 typescript/cli/src/utils/output.ts diff --git a/.changeset/grumpy-ears-relate.md b/.changeset/grumpy-ears-relate.md new file mode 100644 index 000000000..8dbc0a515 --- /dev/null +++ b/.changeset/grumpy-ears-relate.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Adds the warp check command to compare warp routes config files with on chain warp route deployments diff --git a/typescript/cli/src/check/warp.ts b/typescript/cli/src/check/warp.ts new file mode 100644 index 000000000..a31fac62e --- /dev/null +++ b/typescript/cli/src/check/warp.ts @@ -0,0 +1,41 @@ +import { stringify as yamlStringify } from 'yaml'; + +import { WarpRouteDeployConfig, normalizeConfig } from '@hyperlane-xyz/sdk'; +import { ObjectDiff, diffObjMerge } from '@hyperlane-xyz/utils'; + +import { log, logGreen } from '../logger.js'; +import '../utils/output.js'; +import { formatYamlViolationsOutput } from '../utils/output.js'; + +export async function runWarpRouteCheck({ + warpRouteConfig, + onChainWarpConfig, +}: { + warpRouteConfig: WarpRouteDeployConfig; + onChainWarpConfig: WarpRouteDeployConfig; +}): Promise { + // Go through each chain and only add to the output the chains that have mismatches + const [violations, isInvalid] = Object.keys(warpRouteConfig).reduce( + (acc, chain) => { + const { mergedObject, isInvalid } = diffObjMerge( + normalizeConfig(onChainWarpConfig[chain]), + normalizeConfig(warpRouteConfig[chain]), + ); + + if (isInvalid) { + acc[0][chain] = mergedObject; + acc[1] ||= isInvalid; + } + + return acc; + }, + [{}, false] as [{ [index: string]: ObjectDiff }, boolean], + ); + + if (isInvalid) { + log(formatYamlViolationsOutput(yamlStringify(violations, null, 2))); + process.exit(1); + } + + logGreen(`No violations found`); +} diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index 7b145ca44..e72b72452 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -41,7 +41,7 @@ const validateChainCommand: CommandModuleWithContext<{ path: string }> = { command: 'chain', describe: 'Validate a chain config file', builder: { - path: inputFileCommandOption, + path: inputFileCommandOption(), }, handler: async ({ path }) => { readChainConfigs(path); @@ -54,7 +54,7 @@ const validateIsmCommand: CommandModuleWithContext<{ path: string }> = { command: 'ism', describe: 'Validate the basic ISM config file', builder: { - path: inputFileCommandOption, + path: inputFileCommandOption(), }, handler: async ({ path }) => { readMultisigConfig(path); @@ -67,7 +67,7 @@ const validateIsmAdvancedCommand: CommandModuleWithContext<{ path: string }> = { command: 'ism-advanced', describe: 'Validate the advanced ISM config file', builder: { - path: inputFileCommandOption, + path: inputFileCommandOption(), }, handler: async ({ path }) => { readIsmConfig(path); @@ -80,7 +80,7 @@ const validateWarpCommand: CommandModuleWithContext<{ path: string }> = { command: 'warp', describe: 'Validate a Warp Route deployment config file', builder: { - path: inputFileCommandOption, + path: inputFileCommandOption(), }, handler: async ({ path }) => { await readWarpRouteDeployConfig(path); diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index a251445be..218671509 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -91,11 +91,14 @@ export const hookCommandOption: Options = { 'A path to a JSON or YAML file with Hook configs (for every chain)', }; +export const DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH = + './configs/warp-route-deployment.yaml'; + export const warpDeploymentConfigCommandOption: Options = { type: 'string', description: 'A path to a JSON or YAML file with a warp route deployment config.', - default: './configs/warp-route-deployment.yaml', + default: DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, alias: 'wd', }; @@ -134,12 +137,23 @@ export const outputFileCommandOption = ( demandOption, }); -export const inputFileCommandOption: Options = { +interface InputFileCommandOptionConfig + extends Pick { + defaultPath?: string; +} + +export const inputFileCommandOption = ({ + defaultPath, + demandOption = true, + description = 'Input file path', + alias = 'i', +}: InputFileCommandOptionConfig = {}): Options => ({ type: 'string', - description: 'Input file path', - alias: 'i', - demandOption: true, -}; + description, + default: defaultPath, + alias, + demandOption, +}); export const fromAddressCommandOption: Options = { type: 'string', diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index 107db6850..b7fb45624 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -1,24 +1,11 @@ -import { ethers } from 'ethers'; import { stringify as yamlStringify } from 'yaml'; import { CommandModule } from 'yargs'; -import { - HypXERC20Lockbox__factory, - HypXERC20__factory, - IXERC20__factory, -} from '@hyperlane-xyz/core'; -import { - ChainMap, - ChainSubmissionStrategySchema, - EvmERC20WarpRouteReader, - TokenStandard, - WarpCoreConfig, -} from '@hyperlane-xyz/sdk'; -import { objMap, promiseObjAll } from '@hyperlane-xyz/utils'; +import { ChainSubmissionStrategySchema } from '@hyperlane-xyz/sdk'; +import { runWarpRouteCheck } from '../check/warp.js'; import { createWarpRouteDeployConfig, - readWarpCoreConfig, readWarpRouteDeployConfig, } from '../config/warp.js'; import { @@ -27,7 +14,8 @@ import { } from '../context/types.js'; import { evaluateIfDryRunFailure } from '../deploy/dry-run.js'; import { runWarpRouteApply, runWarpRouteDeploy } from '../deploy/warp.js'; -import { log, logGray, logGreen, logRed, logTable } from '../logger.js'; +import { log, logCommandHeader, logGreen } from '../logger.js'; +import { runWarpRouteRead } from '../read/warp.js'; import { sendTestTransfer } from '../send/transfer.js'; import { indentYamlOrJson, @@ -35,13 +23,15 @@ import { removeEndingSlash, writeYamlOrJson, } from '../utils/files.js'; -import { selectRegistryWarpRoute } from '../utils/tokens.js'; +import { getWarpCoreConfigOrExit } from '../utils/input.js'; import { + DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, addressCommandOption, chainCommandOption, dryRunCommandOption, fromAddressCommandOption, + inputFileCommandOption, outputFileCommandOption, strategyCommandOption, symbolCommandOption, @@ -59,6 +49,7 @@ export const warpCommand: CommandModule = { builder: (yargs) => yargs .command(apply) + .command(check) .command(deploy) .command(init) .command(read) @@ -104,17 +95,14 @@ export const apply: CommandModuleWithWriteContext<{ strategy: strategyUrl, receiptsDir, }) => { - logGray(`Hyperlane Warp Apply`); - logGray('--------------------'); // @TODO consider creating a helper function for these dashes - let warpCoreConfig: WarpCoreConfig; - if (symbol) { - warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); - } else if (warp) { - warpCoreConfig = readWarpCoreConfig(warp); - } else { - logRed(`Please specify either a symbol or warp config`); - process.exit(0); - } + logCommandHeader('Hyperlane Warp Apply'); + + const warpCoreConfig = await getWarpCoreConfigOrExit({ + symbol, + warp, + context, + }); + if (strategyUrl) ChainSubmissionStrategySchema.parse(readYamlOrJson(strategyUrl)); const warpDeployConfig = await readWarpRouteDeployConfig(config); @@ -143,8 +131,9 @@ export const deploy: CommandModuleWithWriteContext<{ 'from-address': fromAddressCommandOption, }, handler: async ({ context, config, dryRun }) => { - logGray(`Hyperlane Warp Route Deployment${dryRun ? ' Dry-Run' : ''}`); - logGray('------------------------------------------------'); + logCommandHeader( + `Hyperlane Warp Route Deployment${dryRun ? ' Dry-Run' : ''}`, + ); try { await runWarpRouteDeploy({ @@ -171,11 +160,10 @@ export const init: CommandModuleWithContext<{ describe: 'Create an advanced ISM', default: false, }, - out: outputFileCommandOption('./configs/warp-route-deployment.yaml'), + out: outputFileCommandOption(DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH), }, handler: async ({ context, advanced, out }) => { - logGray('Hyperlane Warp Configure'); - logGray('------------------------'); + logCommandHeader('Hyperlane Warp Configure'); await createWarpRouteDeployConfig({ context, @@ -208,7 +196,7 @@ export const read: CommandModuleWithContext<{ false, ), config: outputFileCommandOption( - './configs/warp-route-deployment.yaml', + DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, false, 'The path to output a Warp Config JSON or YAML file.', ), @@ -220,75 +208,14 @@ export const read: CommandModuleWithContext<{ config: configFilePath, symbol, }) => { - logGray('Hyperlane Warp Reader'); - logGray('---------------------'); - - const { multiProvider } = context; - - let addresses: ChainMap; - if (symbol) { - const warpCoreConfig = await selectRegistryWarpRoute( - context.registry, - symbol, - ); - - // TODO: merge with XERC20TokenAdapter and WarpRouteReader - const xerc20Limits = await Promise.all( - warpCoreConfig.tokens - .filter( - (t) => - t.standard === TokenStandard.EvmHypXERC20 || - t.standard === TokenStandard.EvmHypXERC20Lockbox, - ) - .map(async (t) => { - const provider = multiProvider.getProvider(t.chainName); - const router = t.addressOrDenom!; - const xerc20Address = - t.standard === TokenStandard.EvmHypXERC20Lockbox - ? await HypXERC20Lockbox__factory.connect( - router, - provider, - ).xERC20() - : await HypXERC20__factory.connect( - router, - provider, - ).wrappedToken(); + logCommandHeader('Hyperlane Warp Reader'); - const xerc20 = IXERC20__factory.connect(xerc20Address, provider); - const mint = await xerc20.mintingCurrentLimitOf(router); - const burn = await xerc20.burningCurrentLimitOf(router); - - const formattedLimits = objMap({ mint, burn }, (_, v) => - ethers.utils.formatUnits(v, t.decimals), - ); - - return [t.chainName, formattedLimits]; - }), - ); - if (xerc20Limits.length > 0) { - logGray('xERC20 Limits:'); - logTable(Object.fromEntries(xerc20Limits)); - } - - addresses = Object.fromEntries( - warpCoreConfig.tokens.map((t) => [t.chainName, t.addressOrDenom!]), - ); - } else if (chain && address) { - addresses = { - [chain]: address, - }; - } else { - logGreen(`Please specify either a symbol or chain and address`); - process.exit(0); - } - - const config = await promiseObjAll( - objMap(addresses, async (chain, address) => - new EvmERC20WarpRouteReader(multiProvider, chain).deriveWarpRouteConfig( - address, - ), - ), - ); + const config = await runWarpRouteRead({ + context, + chain, + address, + symbol, + }); if (configFilePath) { writeYamlOrJson(configFilePath, config, 'yaml'); @@ -346,15 +273,11 @@ const send: CommandModuleWithWriteContext< amount, recipient, }) => { - let warpCoreConfig: WarpCoreConfig; - if (symbol) { - warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); - } else if (warp) { - warpCoreConfig = readWarpCoreConfig(warp); - } else { - logRed(`Please specify either a symbol or warp config`); - process.exit(0); - } + const warpCoreConfig = await getWarpCoreConfigOrExit({ + symbol, + warp, + context, + }); await sendTestTransfer({ context, @@ -370,3 +293,44 @@ const send: CommandModuleWithWriteContext< process.exit(0); }, }; + +export const check: CommandModuleWithContext<{ + config: string; + symbol?: string; + warp?: string; +}> = { + command: 'check', + describe: + 'Verifies that a warp route configuration matches the on chain configuration.', + builder: { + symbol: { + ...symbolCommandOption, + demandOption: false, + }, + warp: { + ...warpCoreConfigCommandOption, + demandOption: false, + }, + config: inputFileCommandOption({ + defaultPath: DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, + description: 'The path to a warp route deployment configuration file', + }), + }, + handler: async ({ context, config, symbol, warp }) => { + logCommandHeader('Hyperlane Warp Check'); + + const warpRouteConfig = await readWarpRouteDeployConfig(config, context); + const onChainWarpConfig = await runWarpRouteRead({ + context, + warp, + symbol, + }); + + await runWarpRouteCheck({ + onChainWarpConfig, + warpRouteConfig, + }); + + process.exit(0); + }, +}; diff --git a/typescript/cli/src/logger.ts b/typescript/cli/src/logger.ts index d5347c66d..621d70e4d 100644 --- a/typescript/cli/src/logger.ts +++ b/typescript/cli/src/logger.ts @@ -57,5 +57,9 @@ export const errorRed = (...args: any) => logColor('error', chalk.red, ...args); export const logDebug = (msg: string, ...args: any) => logger.debug(msg, ...args); +export const logCommandHeader = (msg: string) => { + logGray(`${msg}\n${'_'.repeat(msg.length)}`); +}; + // No support for table in pino so print directly to console export const logTable = (...args: any) => console.table(...args); diff --git a/typescript/cli/src/read/warp.ts b/typescript/cli/src/read/warp.ts new file mode 100644 index 000000000..9139d890c --- /dev/null +++ b/typescript/cli/src/read/warp.ts @@ -0,0 +1,117 @@ +import { ethers } from 'ethers'; + +import { + HypXERC20Lockbox__factory, + HypXERC20__factory, + IXERC20__factory, +} from '@hyperlane-xyz/core'; +import { + ChainMap, + ChainName, + EvmERC20WarpRouteReader, + TokenStandard, +} from '@hyperlane-xyz/sdk'; +import { isAddressEvm, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; + +import { CommandContext } from '../context/types.js'; +import { logGray, logRed, logTable } from '../logger.js'; +import { getWarpCoreConfigOrExit } from '../utils/input.js'; + +export async function runWarpRouteRead({ + context, + chain, + address, + warp, + symbol, +}: { + context: CommandContext; + chain?: ChainName; + warp?: string; + address?: string; + symbol?: string; +}): Promise> { + const { multiProvider } = context; + + let addresses: ChainMap; + if (symbol || warp) { + const warpCoreConfig = await getWarpCoreConfigOrExit({ + context, + warp, + symbol, + }); + + // TODO: merge with XERC20TokenAdapter and WarpRouteReader + const xerc20Limits = await Promise.all( + warpCoreConfig.tokens + .filter( + (t) => + t.standard === TokenStandard.EvmHypXERC20 || + t.standard === TokenStandard.EvmHypXERC20Lockbox, + ) + .map(async (t) => { + const provider = multiProvider.getProvider(t.chainName); + const router = t.addressOrDenom!; + const xerc20Address = + t.standard === TokenStandard.EvmHypXERC20Lockbox + ? await HypXERC20Lockbox__factory.connect( + router, + provider, + ).xERC20() + : await HypXERC20__factory.connect( + router, + provider, + ).wrappedToken(); + + const xerc20 = IXERC20__factory.connect(xerc20Address, provider); + const mint = await xerc20.mintingCurrentLimitOf(router); + const burn = await xerc20.burningCurrentLimitOf(router); + + const formattedLimits = objMap({ mint, burn }, (_, v) => + ethers.utils.formatUnits(v, t.decimals), + ); + + return [t.chainName, formattedLimits]; + }), + ); + + if (xerc20Limits.length > 0) { + logGray('xERC20 Limits:'); + logTable(Object.fromEntries(xerc20Limits)); + } + + addresses = Object.fromEntries( + warpCoreConfig.tokens.map((t) => [t.chainName, t.addressOrDenom!]), + ); + } else if (chain && address) { + addresses = { + [chain]: address, + }; + } else { + logRed(`Please specify either a symbol, chain and address or warp file`); + process.exit(1); + } + + // Check if there any non-EVM chains in the config and exit + const nonEvmChains = Object.entries(addresses) + .filter(([_, address]) => !isAddressEvm(address)) + .map(([chain]) => chain); + if (nonEvmChains.length > 0) { + const chainList = nonEvmChains.join(', '); + logRed( + `${chainList} ${ + nonEvmChains.length > 1 ? 'are' : 'is' + } non-EVM and not compatible with the cli`, + ); + process.exit(1); + } + + const config = await promiseObjAll( + objMap(addresses, async (chain, address) => + new EvmERC20WarpRouteReader(multiProvider, chain).deriveWarpRouteConfig( + address, + ), + ), + ); + + return config; +} diff --git a/typescript/cli/src/utils/input.ts b/typescript/cli/src/utils/input.ts index 4b54c4f3e..251c6c0c5 100644 --- a/typescript/cli/src/utils/input.ts +++ b/typescript/cli/src/utils/input.ts @@ -18,9 +18,14 @@ import type { PartialDeep } from '@inquirer/type'; import ansiEscapes from 'ansi-escapes'; import chalk from 'chalk'; -import { logGray } from '../logger.js'; +import { WarpCoreConfig } from '@hyperlane-xyz/sdk'; + +import { readWarpCoreConfig } from '../config/warp.js'; +import { CommandContext } from '../context/types.js'; +import { logGray, logRed } from '../logger.js'; import { indentYamlOrJson } from './files.js'; +import { selectRegistryWarpRoute } from './tokens.js'; export async function detectAndConfirmOrPrompt( detect: () => Promise, @@ -72,6 +77,34 @@ export async function inputWithInfo({ return answer; } +/** + * Gets a {@link WarpCoreConfig} based on the provided path or prompts the user to choose one: + * - if `symbol` is provided the user will have to select one of the available warp routes. + * - if `warp` is provided the config will be read by the provided file path. + * - if none is provided the CLI will exit. + */ +export async function getWarpCoreConfigOrExit({ + context, + symbol, + warp, +}: { + context: CommandContext; + symbol?: string; + warp?: string; +}): Promise { + let warpCoreConfig: WarpCoreConfig; + if (symbol) { + warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); + } else if (warp) { + warpCoreConfig = readWarpCoreConfig(warp); + } else { + logRed(`Please specify either a symbol or warp config`); + process.exit(0); + } + + return warpCoreConfig; +} + /** * Searchable checkbox code * diff --git a/typescript/cli/src/utils/output.ts b/typescript/cli/src/utils/output.ts new file mode 100644 index 000000000..442b8a090 --- /dev/null +++ b/typescript/cli/src/utils/output.ts @@ -0,0 +1,56 @@ +import chalk from 'chalk'; + +export enum ViolationDiffType { + None, + Expected, + Actual, +} + +type FormatterByDiffType = Record string>; + +const defaultDiffFormatter: FormatterByDiffType = { + [ViolationDiffType.Actual]: chalk.red, + [ViolationDiffType.Expected]: chalk.green, + [ViolationDiffType.None]: (text: string) => text, +}; + +/** + * Takes a yaml formatted string and highlights differences by looking at `expected` and `actual` properties. + */ +export function formatYamlViolationsOutput( + yamlString: string, + formatters: FormatterByDiffType = defaultDiffFormatter, +): string { + const lines = yamlString.split('\n'); + + let curr: ViolationDiffType = ViolationDiffType.None; + let lastDiffIndent = 0; + const highlightedLines = lines.map((line) => { + // Get how many white space/tabs we have before the property name + const match = line.match(/^(\s*)/); + const currentIndent = match ? match[0].length : 0; + + let formattedLine = line; + // if the current indentation is smaller than the previous diff one + // we just got out of a diff property and we reset the formatting + if (currentIndent < lastDiffIndent) { + curr = ViolationDiffType.None; + } + + if (line.includes('expected:')) { + lastDiffIndent = currentIndent; + curr = ViolationDiffType.Expected; + formattedLine = line.replace('expected:', 'EXPECTED:'); + } + + if (line.includes('actual:')) { + lastDiffIndent = currentIndent; + curr = ViolationDiffType.Actual; + formattedLine = line.replace('actual:', 'ACTUAL:'); + } + + return formatters[curr](formattedLine); + }); + + return highlightedLines.join('\n'); +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index e80ad1f3e..415a11d26 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -487,7 +487,7 @@ export { setFork, stopImpersonatingAccount, } from './utils/fork.js'; -export { multisigIsmVerificationCost } from './utils/ism.js'; +export { multisigIsmVerificationCost, normalizeConfig } from './utils/ism.js'; export { MultiGeneric } from './utils/MultiGeneric.js'; export { SealevelAccountDataWrapper, diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index e45994f8f..0c18543dd 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -119,6 +119,8 @@ export { pick, promiseObjAll, stringifyObject, + diffObjMerge, + ObjectDiff, } from './objects.js'; export { Result, failure, success } from './result.js'; export { difference, setEquality, symmetricDifference } from './sets.js'; diff --git a/typescript/utils/src/objects.test.ts b/typescript/utils/src/objects.test.ts index b6fd1d012..d5ed72a6b 100644 --- a/typescript/utils/src/objects.test.ts +++ b/typescript/utils/src/objects.test.ts @@ -1,6 +1,12 @@ import { expect } from 'chai'; -import { deepCopy, deepEquals, objMerge, objOmit } from './objects.js'; +import { + deepCopy, + deepEquals, + diffObjMerge, + objMerge, + objOmit, +} from './objects.js'; describe('Object utilities', () => { it('deepEquals', () => { @@ -67,4 +73,132 @@ describe('Object utilities', () => { const omitted1_2 = objOmit(obj1, obj2, 10, false); expect(omitted1_2).to.eql({ a: 1, b: { d: 'string' } }); }); + + describe('diffObjMerge', () => { + it('should merge objects with equal values', () => { + const actual = { a: 1, b: 2 }; + const expected = { a: 1, b: 2 }; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: false, + mergedObject: { a: 1, b: 2 }, + }); + }); + + it('should return a diff for objects with different values', () => { + const actual = { a: 1, b: 2 }; + const expected = { a: 1, b: 3 }; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: true, + mergedObject: { + a: 1, + b: { actual: 2, expected: 3 }, + }, + }); + }); + + it('should detect missing fields in the top level object', () => { + const actual = { a: 1 }; + const expected = { a: 1, b: 3 }; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: true, + mergedObject: { + a: 1, + b: { actual: '', expected: 3 }, + }, + }); + }); + + it('should detect extra fields in the top level object', () => { + const actual = { a: 1, b: 2 }; + const expected = { a: 1 }; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: true, + mergedObject: { + a: 1, + b: { actual: 2, expected: '' }, + }, + }); + }); + + it('should merge nested objects and show differences', () => { + const actual = { a: 1, b: { c: 2, d: 4 } }; + const expected = { a: 1, b: { c: 2, d: 3 } }; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: true, + mergedObject: { + a: 1, + b: { + c: 2, + d: { actual: 4, expected: 3 }, + }, + }, + }); + }); + + it('should throw an error when maxDepth is exceeded', () => { + const actual = { a: { b: { c: { d: { e: 5 } } } } }; + const expected = { a: { b: { c: { d: { e: 5 } } } } }; + + expect(() => diffObjMerge(actual, expected, 3)).to.Throw( + 'diffObjMerge tried to go too deep', + ); + }); + + it('should merge arrays of equal length and show the diffs', () => { + const actual = [1, 2, 3]; + const expected = [1, 2, 4]; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: true, + mergedObject: [1, 2, { actual: 3, expected: 4 }], + }); + }); + + it('should return a diff for arrays of different lengths', () => { + const actual = [1, 2]; + const expected = [1, 2, 3]; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: true, + mergedObject: { + actual, + expected, + }, + }); + }); + + it('should handle null and undefined values properly', () => { + const actual = { a: null, b: 2 }; + const expected = { a: undefined, b: 2 }; + + const result = diffObjMerge(actual, expected); + + expect(result).to.eql({ + isInvalid: false, + mergedObject: { + a: undefined, + b: 2, + }, + }); + }); + }); }); diff --git a/typescript/utils/src/objects.ts b/typescript/utils/src/objects.ts index 680c5d579..403caa849 100644 --- a/typescript/utils/src/objects.ts +++ b/typescript/utils/src/objects.ts @@ -2,6 +2,7 @@ import { cloneDeep, isEqual } from 'lodash-es'; import { stringify as yamlStringify } from 'yaml'; import { ethersBigNumberSerializer } from './logging.js'; +import { isNullish } from './typeof.js'; import { assert } from './validation.js'; export function isObject(item: any) { @@ -216,3 +217,103 @@ export function stringifyObject( } return yamlStringify(JSON.parse(json), null, space); } + +interface ObjectDiffOutput { + actual: any; + expected: any; +} + +export type ObjectDiff = + | { + [key: string]: ObjectDiffOutput | ObjectDiff; + } + | ObjectDiff[] + | undefined; + +/** + * Merges 2 objects showing any difference in value for common fields. + */ +export function diffObjMerge( + actual: Record, + expected: Record, + maxDepth = 10, +): { + mergedObject: ObjectDiff; + isInvalid: boolean; +} { + if (maxDepth === 0) { + throw new Error('diffObjMerge tried to go too deep'); + } + + let isDiff = false; + if (!isObject(actual) && !isObject(expected) && actual === expected) { + return { + isInvalid: isDiff, + mergedObject: actual, + }; + } + + if (isNullish(actual) && isNullish(expected)) { + return { mergedObject: undefined, isInvalid: isDiff }; + } + + if (isObject(actual) && isObject(expected)) { + const ret: Record = {}; + + const actualKeys = new Set(Object.keys(actual)); + const expectedKeys = new Set(Object.keys(expected)); + const allKeys = new Set([...actualKeys, ...expectedKeys]); + for (const key of allKeys.values()) { + if (actualKeys.has(key) && expectedKeys.has(key)) { + const { mergedObject, isInvalid } = + diffObjMerge(actual[key], expected[key], maxDepth - 1) ?? {}; + ret[key] = mergedObject; + isDiff ||= isInvalid; + } else if (actualKeys.has(key) && !isNullish(actual[key])) { + ret[key] = { + actual: actual[key], + expected: '' as any, + }; + isDiff = true; + } else if (!isNullish(expected[key])) { + ret[key] = { + actual: '' as any, + expected: expected[key], + }; + isDiff = true; + } + } + return { + isInvalid: isDiff, + mergedObject: ret, + }; + } + + // Merge the elements of the array to see if there are any differences + if ( + Array.isArray(actual) && + Array.isArray(expected) && + actual.length === expected.length + ) { + const merged = actual.reduce( + (acc: [ObjectDiff[], boolean], curr, idx) => { + const { isInvalid, mergedObject } = diffObjMerge(curr, expected[idx]); + + acc[0].push(mergedObject); + acc[1] ||= isInvalid; + + return acc; + }, + [[], isDiff], + ); + return { + isInvalid: merged[1], + mergedObject: merged[0], + }; + } + + return { + mergedObject: { expected: expected ?? '', actual: actual ?? '' }, + isInvalid: true, + }; +} From c9085afd96c61a74207feff2dc5dbd86ec37ff95 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:38:32 +0530 Subject: [PATCH 21/26] fix: add fix for checking for correct messageId in `OPL2ToL1Ism` and `ArbL2ToL1Ism` via external call (#4437) ### Description - In agreement with Chainlight team's recommendation, added a second isVerified() check after portal call to make sure an arbitrary call which passes the check for metadata length and messageId cannot be verified without calling is verifyMessageId(messageId) in `OPL2ToL1Ism` and `ArbL2ToL1` ### Drive-by changes None ### Related issues - fixes https://github.com/chainlight-io/2024-08-hyperlane/issues/2 ### Backward compatibility No, but the contracts aren't deployed anywhere ### Testing Unit testing --- .changeset/itchy-singers-hang.md | 5 +++++ solidity/contracts/isms/hook/ArbL2ToL1Ism.sol | 1 + solidity/contracts/isms/hook/OPL2ToL1Ism.sol | 4 ++-- solidity/test/isms/ERC5164ISM.t.sol | 2 ++ solidity/test/isms/ExternalBridgeTest.sol | 20 +++++++++++++++++-- solidity/test/isms/OPStackIsm.t.sol | 10 ++++++---- 6 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 .changeset/itchy-singers-hang.md diff --git a/.changeset/itchy-singers-hang.md b/.changeset/itchy-singers-hang.md new file mode 100644 index 000000000..97096ff1a --- /dev/null +++ b/.changeset/itchy-singers-hang.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': patch +--- + +Patched OPL2ToL1Ism to check for correct messageId for external call in verify diff --git a/solidity/contracts/isms/hook/ArbL2ToL1Ism.sol b/solidity/contracts/isms/hook/ArbL2ToL1Ism.sol index a7bd71447..98b5f9bd6 100644 --- a/solidity/contracts/isms/hook/ArbL2ToL1Ism.sol +++ b/solidity/contracts/isms/hook/ArbL2ToL1Ism.sol @@ -63,6 +63,7 @@ contract ArbL2ToL1Ism is ) external override returns (bool) { if (!isVerified(message)) { _verifyWithOutboxCall(metadata, message); + require(isVerified(message), "ArbL2ToL1Ism: message not verified"); } releaseValueToRecipient(message); return true; diff --git a/solidity/contracts/isms/hook/OPL2ToL1Ism.sol b/solidity/contracts/isms/hook/OPL2ToL1Ism.sol index b333b15cd..ef3986861 100644 --- a/solidity/contracts/isms/hook/OPL2ToL1Ism.sol +++ b/solidity/contracts/isms/hook/OPL2ToL1Ism.sol @@ -66,9 +66,9 @@ contract OPL2ToL1Ism is bytes calldata metadata, bytes calldata message ) external override returns (bool) { - bool verified = isVerified(message); - if (!verified) { + if (!isVerified(message)) { _verifyWithPortalCall(metadata, message); + require(isVerified(message), "OPL2ToL1Ism: message not verified"); } releaseValueToRecipient(message); return true; diff --git a/solidity/test/isms/ERC5164ISM.t.sol b/solidity/test/isms/ERC5164ISM.t.sol index 75c79fb82..8063979f4 100644 --- a/solidity/test/isms/ERC5164ISM.t.sol +++ b/solidity/test/isms/ERC5164ISM.t.sol @@ -150,6 +150,8 @@ contract ERC5164IsmTest is ExternalBridgeTest { function test_verify_valueAlreadyClaimed(uint256) public override {} + function test_verify_false_arbitraryCall() public override {} + /* ============ helper functions ============ */ function _externalBridgeDestinationCall( diff --git a/solidity/test/isms/ExternalBridgeTest.sol b/solidity/test/isms/ExternalBridgeTest.sol index 8db043fcf..344e001af 100644 --- a/solidity/test/isms/ExternalBridgeTest.sol +++ b/solidity/test/isms/ExternalBridgeTest.sol @@ -135,14 +135,14 @@ abstract contract ExternalBridgeTest is Test { 1 ether, messageId ); - ism.verify(externalCalldata, encodedMessage); + assertTrue(ism.verify(externalCalldata, encodedMessage)); assertEq(address(testRecipient).balance, 1 ether); } function test_verify_revertsWhen_invalidIsm() public virtual { bytes memory externalCalldata = _encodeExternalDestinationBridgeCall( address(hook), - address(this), + address(hook), 0, messageId ); @@ -217,6 +217,19 @@ abstract contract ExternalBridgeTest is Test { assertEq(address(testRecipient).balance, _msgValue); } + function test_verify_false_arbitraryCall() public virtual { + bytes memory incorrectCalldata = _encodeExternalDestinationBridgeCall( + address(hook), + address(this), + 0, + messageId + ); + + vm.expectRevert(); + ism.verify(incorrectCalldata, encodedMessage); + assertFalse(ism.isVerified(encodedMessage)); + } + /* ============ helper functions ============ */ function _encodeTestMessage() internal view returns (bytes memory) { @@ -265,4 +278,7 @@ abstract contract ExternalBridgeTest is Test { function _setExternalOriginSender( address _sender ) internal virtual returns (bytes memory) {} + + // meant to mock an arbitrary successful call made by the external bridge + function verifyMessageId(bytes32 /*messageId*/) public payable {} } diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index 45c818ec3..3230e59b8 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -133,10 +133,10 @@ contract OPStackIsmTest is ExternalBridgeTest { } function _encodeExternalDestinationBridgeCall( - address _from, - address _to, - uint256 _msgValue, - bytes32 _messageId + address /*_from*/, + address /*_to*/, + uint256 /*_msgValue*/, + bytes32 /*_messageId*/ ) internal pure override returns (bytes memory) { return new bytes(0); } @@ -148,6 +148,8 @@ contract OPStackIsmTest is ExternalBridgeTest { function test_verify_revertsWhen_invalidIsm() public override {} + function test_verify_false_arbitraryCall() public override {} + /* ============ ISM.verifyMessageId ============ */ function test_verify_revertsWhen_notAuthorizedHook() public override { From 2760da1ded71159d8b681b75a3d2a797586e81d4 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:40:09 +0530 Subject: [PATCH 22/26] fix: ensure no duplicate signatures verified for `AbstractWeightedMultisigIsm.verify()` (#4468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description - There's a bug in the verify function of `AbstractWeightedMultisigIsm` that an attacker might use to bypass the verification of multiple signatures. The code tried to check the duplication of the signers if not found, but the code does not increment validatorIndex when the recovered signer matches to the stored signer. For instance: - validatorsAndThresholdWeight returns [A, B, C, D] - an attacker uses signatures as [sig from A, sig from A, sig from A, sig from A, ...] or [sig from B, sig from B, …] Fix is to add a ++validatorIndex at the end of the for loop implies we don't allow the next signer to be the same as the signer we just verified. ### Drive-by changes None ### Related issues From Chainlight's audit findings ### Backward compatibility We haven't deployed these contracts yet on testnet/mainnet ### Testing Fuzz testing --- .../multisig/AbstractWeightedMultisigIsm.sol | 12 ++- solidity/test/isms/MultisigIsm.t.sol | 26 +++++++ solidity/test/isms/WeightedMultisigIsm.t.sol | 75 ++++++++++++++++++- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/solidity/contracts/isms/multisig/AbstractWeightedMultisigIsm.sol b/solidity/contracts/isms/multisig/AbstractWeightedMultisigIsm.sol index 2264b192c..8b7fee049 100644 --- a/solidity/contracts/isms/multisig/AbstractWeightedMultisigIsm.sol +++ b/solidity/contracts/isms/multisig/AbstractWeightedMultisigIsm.sol @@ -73,11 +73,14 @@ abstract contract AbstractStaticWeightedMultisigIsm is // assumes that signatures are ordered by validator for ( - uint256 i = 0; - _totalWeight < _thresholdWeight && i < _validatorCount; - ++i + uint256 signatureIndex = 0; + _totalWeight < _thresholdWeight && signatureIndex < _validatorCount; + ++signatureIndex ) { - address _signer = ECDSA.recover(_digest, signatureAt(_metadata, i)); + address _signer = ECDSA.recover( + _digest, + signatureAt(_metadata, signatureIndex) + ); // loop through remaining validators until we find a match while ( _validatorIndex < _validatorCount && @@ -90,6 +93,7 @@ abstract contract AbstractStaticWeightedMultisigIsm is // add the weight of the current validator _totalWeight += _validators[_validatorIndex].weight; + ++_validatorIndex; } require( _totalWeight >= _thresholdWeight, diff --git a/solidity/test/isms/MultisigIsm.t.sol b/solidity/test/isms/MultisigIsm.t.sol index 8c0b61d38..29098475f 100644 --- a/solidity/test/isms/MultisigIsm.t.sol +++ b/solidity/test/isms/MultisigIsm.t.sol @@ -186,6 +186,32 @@ abstract contract AbstractMultisigIsmTest is Test { metadata[index] = ~metadata[index]; assertFalse(ism.verify(metadata, message)); } + + function test_verify_revertWhen_duplicateSignatures( + uint32 destination, + bytes32 recipient, + bytes calldata body, + uint8 m, + uint8 n, + bytes32 seed + ) public virtual { + vm.assume(1 < m && m <= n && n < 10); + bytes memory message = getMessage(destination, recipient, body); + bytes memory metadata = getMetadata(m, n, seed, message); + + bytes memory duplicateMetadata = new bytes(metadata.length); + for (uint256 i = 0; i < metadata.length - 65; i++) { + duplicateMetadata[i] = metadata[i]; + } + for (uint256 i = 0; i < 65; i++) { + duplicateMetadata[metadata.length - 65 + i] = metadata[ + metadata.length - 130 + i + ]; + } + + vm.expectRevert("!threshold"); + ism.verify(duplicateMetadata, message); + } } contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { diff --git a/solidity/test/isms/WeightedMultisigIsm.t.sol b/solidity/test/isms/WeightedMultisigIsm.t.sol index df2a3d0ea..0c5fd7ee4 100644 --- a/solidity/test/isms/WeightedMultisigIsm.t.sol +++ b/solidity/test/isms/WeightedMultisigIsm.t.sol @@ -65,7 +65,6 @@ abstract contract AbstractStaticWeightedMultisigIsmTest is } } - // ism = IInterchainSecurityModule(deployedIsm); ism = IInterchainSecurityModule( weightedFactory.deploy(validators, threshold) ); @@ -136,7 +135,7 @@ abstract contract AbstractStaticWeightedMultisigIsmTest is return metadata; } - function testVerify_revertInsufficientWeight( + function test_verify_revertInsufficientWeight( uint32 destination, bytes32 recipient, bytes calldata body, @@ -161,6 +160,34 @@ abstract contract AbstractStaticWeightedMultisigIsmTest is vm.expectRevert("Insufficient validator weight"); ism.verify(insufficientMetadata, message); } + + function test_verify_revertWhen_duplicateSignatures( + uint32 destination, + bytes32 recipient, + bytes calldata body, + uint8 m, + uint8 n, + bytes32 seed + ) public virtual override { + vm.assume(1 < m && m <= n && n < 10); + bytes memory message = getMessage(destination, recipient, body); + bytes memory metadata = getMetadata(m, n, seed, message); + + bytes memory duplicateMetadata = new bytes(metadata.length); + for (uint256 i = 0; i < metadata.length - 65; i++) { + duplicateMetadata[i] = metadata[i]; + } + for (uint256 i = 0; i < 65; i++) { + duplicateMetadata[metadata.length - 65 + i] = metadata[ + metadata.length - 130 + i + ]; + } + + if (weightedIsm.signatureCount(metadata) >= 2) { + vm.expectRevert("Invalid signer"); + ism.verify(duplicateMetadata, message); + } + } } contract StaticMerkleRootWeightedMultisigIsmTest is @@ -194,6 +221,28 @@ contract StaticMerkleRootWeightedMultisigIsmTest is message ); } + + function test_verify_revertWhen_duplicateSignatures( + uint32 destination, + bytes32 recipient, + bytes calldata body, + uint8 m, + uint8 n, + bytes32 seed + ) + public + override(AbstractMultisigIsmTest, AbstractStaticWeightedMultisigIsmTest) + { + AbstractStaticWeightedMultisigIsmTest + .test_verify_revertWhen_duplicateSignatures( + destination, + recipient, + body, + m, + n, + seed + ); + } } contract StaticMessageIdWeightedMultisigIsmTest is @@ -227,4 +276,26 @@ contract StaticMessageIdWeightedMultisigIsmTest is message ); } + + function test_verify_revertWhen_duplicateSignatures( + uint32 destination, + bytes32 recipient, + bytes calldata body, + uint8 m, + uint8 n, + bytes32 seed + ) + public + override(AbstractMultisigIsmTest, AbstractStaticWeightedMultisigIsmTest) + { + AbstractStaticWeightedMultisigIsmTest + .test_verify_revertWhen_duplicateSignatures( + destination, + recipient, + body, + m, + n, + seed + ); + } } From ec6b874b15fcdee8ddbd189fb714866f2c5df6be Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:42:06 +0530 Subject: [PATCH 23/26] feat(contracts): add nonce for monotonically increasing delivery ordering for `HypERC4626` (#4534) ### Description - Added a `rateUpdateNonce` in `HypERC4626Collateral` and `previousNonce` in `HypERC4626` to ensure we only update the exchangeRate on the synthetic asset if the update was after the last recorded update. This is to make sure we don't update it to a stale exchange rate which may cause losses to users using the synthetic asset. ### Drive-by changes - `processInboundMessage` in MockMailbox` to simulate processing of messages out of order. ### Related issues - fixes https://github.com/chainlight-io/2024-08-hyperlane/issues/12 ### Backward compatibility Yes ### Testing Unit test --- .changeset/red-actors-shop.md | 5 ++ solidity/contracts/mock/MockMailbox.sol | 5 ++ .../contracts/token/extensions/HypERC4626.sol | 14 +++- .../token/extensions/HypERC4626Collateral.sol | 9 ++- solidity/test/token/HypERC4626Test.t.sol | 69 +++++++++++++++++-- 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 .changeset/red-actors-shop.md diff --git a/.changeset/red-actors-shop.md b/.changeset/red-actors-shop.md new file mode 100644 index 000000000..0ee301e90 --- /dev/null +++ b/.changeset/red-actors-shop.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': patch +--- + +Added nonce to HypERC4626 diff --git a/solidity/contracts/mock/MockMailbox.sol b/solidity/contracts/mock/MockMailbox.sol index ad212dcef..c4b4b63e9 100644 --- a/solidity/contracts/mock/MockMailbox.sol +++ b/solidity/contracts/mock/MockMailbox.sol @@ -77,4 +77,9 @@ contract MockMailbox is Mailbox { Mailbox(address(this)).process{value: msg.value}("", _message); inboundProcessedNonce++; } + + function processInboundMessage(uint32 _nonce) public { + bytes memory _message = inboundMessages[_nonce]; + Mailbox(address(this)).process("", _message); + } } diff --git a/solidity/contracts/token/extensions/HypERC4626.sol b/solidity/contracts/token/extensions/HypERC4626.sol index 2252696fa..141b081ba 100644 --- a/solidity/contracts/token/extensions/HypERC4626.sol +++ b/solidity/contracts/token/extensions/HypERC4626.sol @@ -17,9 +17,12 @@ contract HypERC4626 is HypERC20 { using Message for bytes; using TokenMessage for bytes; + event ExchangeRateUpdated(uint256 newExchangeRate, uint32 rateUpdateNonce); + uint256 public constant PRECISION = 1e10; uint32 public immutable collateralDomain; uint256 public exchangeRate; // 1e10 + uint32 public previousNonce; constructor( uint8 _decimals, @@ -66,7 +69,16 @@ contract HypERC4626 is HypERC20 { bytes calldata _message ) internal virtual override { if (_origin == collateralDomain) { - exchangeRate = abi.decode(_message.metadata(), (uint256)); + (uint256 newExchangeRate, uint32 rateUpdateNonce) = abi.decode( + _message.metadata(), + (uint256, uint32) + ); + // only update if the nonce is greater than the previous nonce + if (rateUpdateNonce > previousNonce) { + exchangeRate = newExchangeRate; + previousNonce = rateUpdateNonce; + emit ExchangeRateUpdated(exchangeRate, rateUpdateNonce); + } } super._handle(_origin, _sender, _message); } diff --git a/solidity/contracts/token/extensions/HypERC4626Collateral.sol b/solidity/contracts/token/extensions/HypERC4626Collateral.sol index 8a084134c..0ae32ab2f 100644 --- a/solidity/contracts/token/extensions/HypERC4626Collateral.sol +++ b/solidity/contracts/token/extensions/HypERC4626Collateral.sol @@ -20,6 +20,8 @@ contract HypERC4626Collateral is HypERC20Collateral { uint256 public constant PRECISION = 1e10; bytes32 public constant NULL_RECIPIENT = 0x0000000000000000000000000000000000000000000000000000000000000001; + // Nonce for the rate update, to ensure sequential updates + uint32 public rateUpdateNonce; constructor( ERC4626 _vault, @@ -52,7 +54,12 @@ contract HypERC4626Collateral is HypERC20Collateral { vault.totalSupply(), Math.Rounding.Down ); - bytes memory _tokenMetadata = abi.encode(_exchangeRate); + + rateUpdateNonce++; + bytes memory _tokenMetadata = abi.encode( + _exchangeRate, + rateUpdateNonce + ); bytes memory _tokenMessage = TokenMessage.format( _recipient, diff --git a/solidity/test/token/HypERC4626Test.t.sol b/solidity/test/token/HypERC4626Test.t.sol index d09e0aae6..338d39b75 100644 --- a/solidity/test/token/HypERC4626Test.t.sol +++ b/solidity/test/token/HypERC4626Test.t.sol @@ -43,6 +43,8 @@ contract HypERC4626CollateralTest is HypTokenTest { HypERC4626 remoteRebasingToken; HypERC4626 peerRebasingToken; + event ExchangeRateUpdated(uint256 newExchangeRate, uint32 rateUpdateNonce); + function setUp() public override { super.setUp(); @@ -95,6 +97,7 @@ contract HypERC4626CollateralTest is HypTokenTest { peerRebasingToken = HypERC4626(address(peerToken)); primaryToken.transfer(ALICE, 1000e18); + primaryToken.transfer(BOB, 1000e18); uint32[] memory domains = new uint32[](3); domains[0] = ORIGIN; @@ -146,6 +149,47 @@ contract HypERC4626CollateralTest is HypTokenTest { ); } + function testRebase_exchangeRateUpdateInSequence() public { + _performRemoteTransferWithoutExpectation(0, transferAmount); + _accrueYield(); + + uint256 exchangeRateInitially = remoteRebasingToken.exchangeRate(); + + vm.startPrank(BOB); + primaryToken.approve(address(localToken), transferAmount); + localToken.transferRemote( + DESTINATION, + BOB.addressToBytes32(), + transferAmount + ); + vm.stopPrank(); + + _accrueYield(); + + vm.startPrank(ALICE); + primaryToken.approve(address(localToken), transferAmount); + localToken.transferRemote( + DESTINATION, + BOB.addressToBytes32(), + transferAmount + ); + vm.stopPrank(); + + // process ALICE's transfer + + vm.expectEmit(true, true, true, true); + emit ExchangeRateUpdated(10721400472, 3); + remoteMailbox.processInboundMessage(2); + uint256 exchangeRateBefore = remoteRebasingToken.exchangeRate(); + + // process BOB's transfer + remoteMailbox.processInboundMessage(1); + uint256 exchangeRateAfter = remoteRebasingToken.exchangeRate(); + + assertLt(exchangeRateInitially, exchangeRateBefore); // updates bc nonce=2 is after nonce=0 + assertEq(exchangeRateBefore, exchangeRateAfter); // doesn't update bc nonce=1 is before nonce=0 + } + function testSyntheticTransfers_withRebase() public { _performRemoteTransferWithoutExpectation(0, transferAmount); assertEq(remoteToken.balanceOf(BOB), transferAmount); @@ -173,6 +217,7 @@ contract HypERC4626CollateralTest is HypTokenTest { } function testWithdrawalWithoutYield() public { + uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB); _performRemoteTransferWithoutExpectation(0, transferAmount); assertEq(remoteToken.balanceOf(BOB), transferAmount); @@ -183,10 +228,14 @@ contract HypERC4626CollateralTest is HypTokenTest { transferAmount ); localMailbox.processNextInboundMessage(); - assertEq(primaryToken.balanceOf(BOB), transferAmount); + assertEq( + primaryToken.balanceOf(BOB) - bobPrimaryBefore, + transferAmount + ); } function testWithdrawalWithYield() public { + uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB); _performRemoteTransferWithoutExpectation(0, transferAmount); assertEq(remoteToken.balanceOf(BOB), transferAmount); @@ -205,13 +254,22 @@ contract HypERC4626CollateralTest is HypTokenTest { uint256 _expectedBal = transferAmount + _discountedYield(); // BOB gets the yield even though it didn't rebase - assertApproxEqRelDecimal(_bobBal, _expectedBal, 1e14, 0); - assertTrue(_bobBal < _expectedBal, "Transfer remote should round down"); + assertApproxEqRelDecimal( + _bobBal - bobPrimaryBefore, + _expectedBal, + 1e14, + 0 + ); + assertTrue( + _bobBal - bobPrimaryBefore < _expectedBal, + "Transfer remote should round down" + ); assertEq(vault.accumulatedFees(), YIELD / 10); } function testWithdrawalAfterYield() public { + uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB); _performRemoteTransferWithoutExpectation(0, transferAmount); assertEq(remoteToken.balanceOf(BOB), transferAmount); @@ -230,7 +288,7 @@ contract HypERC4626CollateralTest is HypTokenTest { ); localMailbox.processNextInboundMessage(); assertApproxEqRelDecimal( - primaryToken.balanceOf(BOB), + primaryToken.balanceOf(BOB) - bobPrimaryBefore, transferAmount + _discountedYield(), 1e14, 0 @@ -287,6 +345,7 @@ contract HypERC4626CollateralTest is HypTokenTest { } function testWithdrawalAfterDrawdown() public { + uint256 bobPrimaryBefore = primaryToken.balanceOf(BOB); _performRemoteTransferWithoutExpectation(0, transferAmount); assertEq(remoteToken.balanceOf(BOB), transferAmount); @@ -306,7 +365,7 @@ contract HypERC4626CollateralTest is HypTokenTest { ); localMailbox.processNextInboundMessage(); assertApproxEqRelDecimal( - primaryToken.balanceOf(BOB), + primaryToken.balanceOf(BOB) - bobPrimaryBefore, transferAmount - drawdown, 1e14, 0 From 3e1ab756433479602e719fd0cd56979156cbde78 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:44:38 +0530 Subject: [PATCH 24/26] fix(contracts): add check for valid mailbox and relayer in `TrustedRelayerIsm` (#4553) ### Description - minor fix: add an isContract check for mailbox and non-zero check for relayer when instantiating TrustedRelayerIsm ### Drive-by changes None ### Related issues - partly fixes https://github.com/chainlight-io/2024-08-hyperlane/issues/14 ### Backward compatibility Yes ### Testing Unit --- solidity/contracts/isms/TrustedRelayerIsm.sol | 9 +++++++++ solidity/test/isms/TrustedRelayerIsm.t.sol | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/solidity/contracts/isms/TrustedRelayerIsm.sol b/solidity/contracts/isms/TrustedRelayerIsm.sol index aba894a94..87da1bb60 100644 --- a/solidity/contracts/isms/TrustedRelayerIsm.sol +++ b/solidity/contracts/isms/TrustedRelayerIsm.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {IInterchainSecurityModule} from "../interfaces/IInterchainSecurityModule.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Message} from "../libs/Message.sol"; import {Mailbox} from "../Mailbox.sol"; import {PackageVersioned} from "contracts/PackageVersioned.sol"; @@ -15,6 +16,14 @@ contract TrustedRelayerIsm is IInterchainSecurityModule, PackageVersioned { address public immutable trustedRelayer; constructor(address _mailbox, address _trustedRelayer) { + require( + _trustedRelayer != address(0), + "TrustedRelayerIsm: invalid relayer" + ); + require( + Address.isContract(_mailbox), + "TrustedRelayerIsm: invalid mailbox" + ); mailbox = Mailbox(_mailbox); trustedRelayer = _trustedRelayer; } diff --git a/solidity/test/isms/TrustedRelayerIsm.t.sol b/solidity/test/isms/TrustedRelayerIsm.t.sol index 51c574ba1..f630b6474 100644 --- a/solidity/test/isms/TrustedRelayerIsm.t.sol +++ b/solidity/test/isms/TrustedRelayerIsm.t.sol @@ -29,6 +29,13 @@ contract TrustedRelayerIsmTest is Test { recipient.setInterchainSecurityModule(address(ism)); } + function test_revertsWhen_invalidMailboxOrRelayer() public { + vm.expectRevert("TrustedRelayerIsm: invalid relayer"); + new TrustedRelayerIsm(address(mailbox), address(0)); + vm.expectRevert("TrustedRelayerIsm: invalid mailbox"); + new TrustedRelayerIsm(relayer, relayer); + } + function test_verify( uint32 origin, bytes32 sender, From c9bd7c3c52416f1c65cb413f33d422dfcc974d0f Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:49:02 +0530 Subject: [PATCH 25/26] fix(contracts): `RateLimit` minor changes (#4575) ### Description - Added a check for invalid capacity and event for token level change ### Drive-by changes None ### Related issues - fixes https://github.com/chainlight-io/2024-08-hyperlane/issues/14 ### Backward compatibility Yes ### Testing Unit tests --- solidity/contracts/libs/RateLimited.sol | 42 +++++++++++++++++----- solidity/test/lib/RateLimited.t.sol | 47 ++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/solidity/contracts/libs/RateLimited.sol b/solidity/contracts/libs/RateLimited.sol index 4dfb8b262..c58c60379 100644 --- a/solidity/contracts/libs/RateLimited.sol +++ b/solidity/contracts/libs/RateLimited.sol @@ -1,5 +1,19 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ External Imports ============ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /** @@ -7,16 +21,26 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own * @notice A contract used to keep track of an address sender's token amount limits. * @dev Implements a modified token bucket algorithm where the bucket is full in the beginning and gradually refills * See: https://dev.to/satrobit/rate-limiting-using-the-token-bucket-algorithm-3cjh - **/ + * + */ contract RateLimited is OwnableUpgradeable { uint256 public constant DURATION = 1 days; // 86400 - uint256 public filledLevel; /// @notice Current filled level - uint256 public refillRate; /// @notice Tokens per second refill rate - uint256 public lastUpdated; /// @notice Timestamp of the last time an action has been taken TODO prob can be uint40 + /// @notice Current filled level + uint256 public filledLevel; + /// @notice Tokens per second refill rate + uint256 public refillRate; + /// @notice Timestamp of the last time an action has been taken + uint256 public lastUpdated; event RateLimitSet(uint256 _oldCapacity, uint256 _newCapacity); + event ConsumedFilledLevel(uint256 filledLevel, uint256 lastUpdated); + constructor(uint256 _capacity) { + require( + _capacity >= DURATION, + "Capacity must be greater than DURATION" + ); _transferOwnership(msg.sender); setRefillRate(_capacity); filledLevel = _capacity; @@ -88,20 +112,22 @@ contract RateLimited is OwnableUpgradeable { /** * Validate an amount and decreases the currentCapacity - * @param _newAmount The amount to consume the fill level + * @param _consumedAmount The amount to consume the fill level * @return The new filled level */ function validateAndConsumeFilledLevel( - uint256 _newAmount + uint256 _consumedAmount ) public returns (uint256) { uint256 adjustedFilledLevel = calculateCurrentLevel(); - require(_newAmount <= adjustedFilledLevel, "RateLimitExceeded"); + require(_consumedAmount <= adjustedFilledLevel, "RateLimitExceeded"); // Reduce the filledLevel and update lastUpdated - uint256 _filledLevel = adjustedFilledLevel - _newAmount; + uint256 _filledLevel = adjustedFilledLevel - _consumedAmount; filledLevel = _filledLevel; lastUpdated = block.timestamp; + emit ConsumedFilledLevel(filledLevel, lastUpdated); + return _filledLevel; } } diff --git a/solidity/test/lib/RateLimited.t.sol b/solidity/test/lib/RateLimited.t.sol index 1276c86af..d0ea273de 100644 --- a/solidity/test/lib/RateLimited.t.sol +++ b/solidity/test/lib/RateLimited.t.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT or Apache-2.0 pragma solidity ^0.8.13; + import {Test} from "forge-std/Test.sol"; import {RateLimited} from "../../contracts/libs/RateLimited.sol"; @@ -13,8 +14,13 @@ contract RateLimitLibTest is Test { rateLimited = new RateLimited(MAX_CAPACITY); } + function testConstructor_revertsWhen_lowCapacity() public { + vm.expectRevert("Capacity must be greater than DURATION"); + new RateLimited(1 days - 1); + } + function testRateLimited_setsNewLimit() external { - rateLimited.setRefillRate(2 ether); + assert(rateLimited.setRefillRate(2 ether) > 0); assertApproxEqRel(rateLimited.maxCapacity(), 2 ether, ONE_PERCENT); assertEq(rateLimited.refillRate(), uint256(2 ether) / 1 days); // 2 ether / 1 day } @@ -45,6 +51,25 @@ contract RateLimitLibTest is Test { rateLimited.setRefillRate(1 ether); } + function testConsumedFilledLevelEvent() public { + uint256 consumeAmount = 0.5 ether; + + vm.expectEmit(true, true, false, true); + emit RateLimited.ConsumedFilledLevel( + 499999999999993600, + block.timestamp + ); // precision loss + rateLimited.validateAndConsumeFilledLevel(consumeAmount); + + assertApproxEqRelDecimal( + rateLimited.filledLevel(), + MAX_CAPACITY - consumeAmount, + 1e14, + 0 + ); + assertEq(rateLimited.lastUpdated(), block.timestamp); + } + function testRateLimited_neverReturnsGtMaxLimit( uint256 _newAmount, uint40 _newTime @@ -104,4 +129,24 @@ contract RateLimitLibTest is Test { currentTargetLimit = rateLimited.calculateCurrentLevel(); assertApproxEqRel(currentTargetLimit, MAX_CAPACITY, ONE_PERCENT); } + + function testCalculateCurrentLevel_revertsWhenCapacityIsZero() public { + rateLimited.setRefillRate(0); + + vm.expectRevert("RateLimitNotSet"); + rateLimited.calculateCurrentLevel(); + } + + function testValidateAndConsumeFilledLevel_revertsWhenExceedingLimit() + public + { + vm.warp(1 days); + uint256 initialLevel = rateLimited.calculateCurrentLevel(); + + uint256 excessAmount = initialLevel + 1 ether; + + vm.expectRevert("RateLimitExceeded"); + rateLimited.validateAndConsumeFilledLevel(excessAmount); + assertEq(rateLimited.calculateCurrentLevel(), initialLevel); + } } From 72c23c0d636dd9dc659542e129ea2f836255c50f Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:23:22 +0530 Subject: [PATCH 26/26] fix(contracts): owner vault metadata compatibility (#4566) ### Description - Added PRECISION and rateUpdateNonce to ensure compatibility of HypERC4626 (even though this is not a useful warp route bc exchangeRate will stay at 1e10) ### Drive-by changes None ### Related issues - closes https://github.com/chainlight-io/2024-08-hyperlane/issues/9 ### Backward compatibility No ### Testing Unit test --- .changeset/sweet-humans-argue.md | 5 ++ .../contracts/token/extensions/HypERC4626.sol | 17 ++++++- .../token/extensions/HypERC4626Collateral.sol | 19 +++++++- .../extensions/HypERC4626OwnerCollateral.sol | 27 +++++++++-- .../HypERC20CollateralVaultDeposit.t.sol | 47 +++++++++++++++++++ 5 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 .changeset/sweet-humans-argue.md diff --git a/.changeset/sweet-humans-argue.md b/.changeset/sweet-humans-argue.md new file mode 100644 index 000000000..3a6ff4647 --- /dev/null +++ b/.changeset/sweet-humans-argue.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': minor +--- + +Added PRECISION and rateUpdateNonce to ensure compatibility of HypERC4626 diff --git a/solidity/contracts/token/extensions/HypERC4626.sol b/solidity/contracts/token/extensions/HypERC4626.sol index 141b081ba..9ceb5536b 100644 --- a/solidity/contracts/token/extensions/HypERC4626.sol +++ b/solidity/contracts/token/extensions/HypERC4626.sol @@ -1,13 +1,28 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ import {IXERC20} from "../interfaces/IXERC20.sol"; import {HypERC20} from "../HypERC20.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Message} from "../../libs/Message.sol"; import {TokenMessage} from "../libs/TokenMessage.sol"; import {TokenRouter} from "../libs/TokenRouter.sol"; +// ============ External Imports ============ +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + /** * @title Hyperlane ERC20 Rebasing Token * @author Abacus Works diff --git a/solidity/contracts/token/extensions/HypERC4626Collateral.sol b/solidity/contracts/token/extensions/HypERC4626Collateral.sol index 0ae32ab2f..87528b109 100644 --- a/solidity/contracts/token/extensions/HypERC4626Collateral.sol +++ b/solidity/contracts/token/extensions/HypERC4626Collateral.sol @@ -1,11 +1,26 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.8.0; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ import {TokenMessage} from "../libs/TokenMessage.sol"; import {HypERC20Collateral} from "../HypERC20Collateral.sol"; import {TypeCasts} from "../../libs/TypeCasts.sol"; +// ============ External Imports ============ +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + /** * @title Hyperlane ERC4626 Token Collateral with deposits collateral to a vault * @author Abacus Works @@ -17,7 +32,9 @@ contract HypERC4626Collateral is HypERC20Collateral { // Address of the ERC4626 compatible vault ERC4626 public immutable vault; + // Precision for the exchange rate uint256 public constant PRECISION = 1e10; + // Null recipient for rebase transfer bytes32 public constant NULL_RECIPIENT = 0x0000000000000000000000000000000000000000000000000000000000000001; // Nonce for the rate update, to ensure sequential updates diff --git a/solidity/contracts/token/extensions/HypERC4626OwnerCollateral.sol b/solidity/contracts/token/extensions/HypERC4626OwnerCollateral.sol index 1d4d64b0b..42d52f42c 100644 --- a/solidity/contracts/token/extensions/HypERC4626OwnerCollateral.sol +++ b/solidity/contracts/token/extensions/HypERC4626OwnerCollateral.sol @@ -1,9 +1,24 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.8.0; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ import {HypERC20Collateral} from "../HypERC20Collateral.sol"; +// ============ External Imports ============ +import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + /** * @title Hyperlane ERC20 Token Collateral with deposits collateral to a vault, the yield goes to the owner * @author ltyu @@ -11,9 +26,12 @@ import {HypERC20Collateral} from "../HypERC20Collateral.sol"; contract HypERC4626OwnerCollateral is HypERC20Collateral { // Address of the ERC4626 compatible vault ERC4626 public immutable vault; - + // standby precision for exchange rate + uint256 public constant PRECISION = 1e10; // Internal balance of total asset deposited uint256 public assetDeposited; + // Nonce for the rate update, to ensure sequential updates (not necessary for Owner variant but for compatibility with HypERC4626) + uint32 public rateUpdateNonce; event ExcessSharesSwept(uint256 amount, uint256 assetsRedeemed); @@ -40,8 +58,11 @@ contract HypERC4626OwnerCollateral is HypERC20Collateral { function _transferFromSender( uint256 _amount ) internal override returns (bytes memory metadata) { - metadata = super._transferFromSender(_amount); + super._transferFromSender(_amount); _depositIntoVault(_amount); + rateUpdateNonce++; + + return abi.encode(PRECISION, rateUpdateNonce); } /** diff --git a/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol b/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol index 3dba941b3..8d2f9226e 100644 --- a/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol +++ b/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol @@ -16,8 +16,11 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {HypERC4626} from "../../contracts/token/extensions/HypERC4626.sol"; + import {ERC4626Test} from "../../contracts/test/ERC4626/ERC4626Test.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {TokenMessage} from "../../contracts/token/libs/TokenMessage.sol"; import {HypTokenTest} from "./HypERC20.t.sol"; import {HypERC4626OwnerCollateral} from "../../contracts/token/extensions/HypERC4626OwnerCollateral.sol"; @@ -227,6 +230,20 @@ contract HypERC4626OwnerCollateralTest is HypTokenTest { ); } + function testERC4626VaultDeposit_TransferFromSender_CorrectMetadata() + public + { + remoteToken = new HypERC4626(18, address(remoteMailbox), ORIGIN); + _enrollRemoteTokenRouter(); + vm.prank(ALICE); + + primaryToken.approve(address(localToken), TRANSFER_AMT); + _performRemoteTransfer(0, TRANSFER_AMT, 1); + + assertEq(HypERC4626(address(remoteToken)).exchangeRate(), 1e10); + assertEq(HypERC4626(address(remoteToken)).previousNonce(), 1); + } + function testBenchmark_overheadGasUsage() public override { vm.prank(ALICE); primaryToken.approve(address(localToken), TRANSFER_AMT); @@ -243,4 +260,34 @@ contract HypERC4626OwnerCollateralTest is HypTokenTest { uint256 gasAfter = gasleft(); console.log("Overhead gas usage: %d", gasBefore - gasAfter); } + + function _performRemoteTransfer( + uint256 _msgValue, + uint256 _amount, + uint32 _nonce + ) internal { + vm.prank(ALICE); + localToken.transferRemote{value: _msgValue}( + DESTINATION, + BOB.addressToBytes32(), + _amount + ); + + vm.expectEmit(true, true, false, true); + emit ReceivedTransferRemote(ORIGIN, BOB.addressToBytes32(), _amount); + bytes memory _tokenMessage = TokenMessage.format( + BOB.addressToBytes32(), + _amount, + abi.encode(uint256(1e10), _nonce) + ); + + vm.prank(address(remoteMailbox)); + remoteToken.handle( + ORIGIN, + address(localToken).addressToBytes32(), + _tokenMessage + ); + + assertEq(remoteToken.balanceOf(BOB), _amount); + } }