diff --git a/.changeset/afraid-dryers-grab.md b/.changeset/afraid-dryers-grab.md deleted file mode 100644 index ba3d7b1cba..0000000000 --- a/.changeset/afraid-dryers-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Add EvmCoreReader, minor updates. diff --git a/.changeset/blue-eyes-hunt.md b/.changeset/blue-eyes-hunt.md deleted file mode 100644 index f1fd110ecf..0000000000 --- a/.changeset/blue-eyes-hunt.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@hyperlane-xyz/cli': minor -'@hyperlane-xyz/sdk': minor ---- - -Update the warp-route-deployment.yaml to a more sensible schema. This schema sets us up to allow multi-chain collateral deployments. Removes intermediary config objects by using zod instead. diff --git a/.changeset/cool-readers-kiss.md b/.changeset/cool-readers-kiss.md deleted file mode 100644 index fbb85f515b..0000000000 --- a/.changeset/cool-readers-kiss.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Added RPC `concurrency` property to `ChainMetadata`. -Added `CrudModule` abstraction and related types. -Removed `Fuel` ProtocolType. diff --git a/.changeset/early-crabs-float.md b/.changeset/early-crabs-float.md deleted file mode 100644 index 2caae83a36..0000000000 --- a/.changeset/early-crabs-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/cli': minor ---- - -Adds single-chain dry-run support for deploying warp routes & gas estimation for core and warp route dry-run deployments. diff --git a/.changeset/eight-cherries-develop.md b/.changeset/eight-cherries-develop.md deleted file mode 100644 index eabd1d55d0..0000000000 --- a/.changeset/eight-cherries-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Add EvmERC20WarpRouterReader to derive WarpConfig from TokenRouter address diff --git a/.changeset/four-brooms-develop.md b/.changeset/four-brooms-develop.md deleted file mode 100644 index f7dffbc92b..0000000000 --- a/.changeset/four-brooms-develop.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@hyperlane-xyz/cli': minor -'@hyperlane-xyz/sdk': minor ---- - -Add --self-relay to CLI commands diff --git a/.changeset/green-ads-live.md b/.changeset/green-ads-live.md new file mode 100644 index 0000000000..f847f584b6 --- /dev/null +++ b/.changeset/green-ads-live.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Default to home directory for local registry diff --git a/.changeset/heavy-crews-rest.md b/.changeset/heavy-crews-rest.md deleted file mode 100644 index 7343bd2514..0000000000 --- a/.changeset/heavy-crews-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Adding ICA for governance diff --git a/.changeset/hip-toys-warn.md b/.changeset/hip-toys-warn.md deleted file mode 100644 index c7860068be..0000000000 --- a/.changeset/hip-toys-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/infra': minor ---- - -Moved Hook/ISM reading into CLI. diff --git a/.changeset/khaki-days-float.md b/.changeset/khaki-days-float.md deleted file mode 100644 index 137910ce8a..0000000000 --- a/.changeset/khaki-days-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': patch ---- - -Allow gasLimit overrides in the SDK/CLI for deploy txs diff --git a/.changeset/kind-panthers-clap.md b/.changeset/kind-panthers-clap.md deleted file mode 100644 index c3a12d4763..0000000000 --- a/.changeset/kind-panthers-clap.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@hyperlane-xyz/helloworld': minor -'@hyperlane-xyz/utils': minor -'@hyperlane-xyz/cli': minor -'@hyperlane-xyz/sdk': minor -'@hyperlane-xyz/core': minor ---- - -Convert all public hyperlane npm packages from CJS to pure ESM diff --git a/.changeset/lemon-horses-swim.md b/.changeset/lemon-horses-swim.md new file mode 100644 index 0000000000..fe5b1c9538 --- /dev/null +++ b/.changeset/lemon-horses-swim.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': patch +--- + +Improve defaults in chain config command diff --git a/.changeset/moody-colts-dress.md b/.changeset/moody-colts-dress.md deleted file mode 100644 index 0be0e6a985..0000000000 --- a/.changeset/moody-colts-dress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Remove consts such as chainMetadata from SDK diff --git a/.changeset/nice-pianos-tease.md b/.changeset/nice-pianos-tease.md deleted file mode 100644 index 6c8fafbf1f..0000000000 --- a/.changeset/nice-pianos-tease.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@hyperlane-xyz/cli': minor -'@hyperlane-xyz/sdk': minor ---- - -Migrate fork util from CLI to SDK. Anvil IP & Port are now optionally passed into fork util by client. diff --git a/.changeset/nine-masks-guess.md b/.changeset/nine-masks-guess.md deleted file mode 100644 index 3e34519c7b..0000000000 --- a/.changeset/nine-masks-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/cli': minor ---- - -Restructure CLI params around registries diff --git a/.changeset/odd-books-think.md b/.changeset/odd-books-think.md deleted file mode 100644 index 3929f5cbde..0000000000 --- a/.changeset/odd-books-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/cli': minor ---- - -Introduces `hyperlane hook read` and `hyperlane ism read` commands for deriving onchain Hook/ISM configs from an address on a given chain. diff --git a/.changeset/poor-kings-fold.md b/.changeset/poor-kings-fold.md deleted file mode 100644 index 4f24dc3f9e..0000000000 --- a/.changeset/poor-kings-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/utils': patch ---- - -Add objLength and isObjEmpty utils diff --git a/.changeset/sour-bats-sort.md b/.changeset/sour-bats-sort.md new file mode 100644 index 0000000000..37d54bb108 --- /dev/null +++ b/.changeset/sour-bats-sort.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/utils': minor +'@hyperlane-xyz/sdk': minor +--- + +Implement aggregation and multisig ISM metadata encoding diff --git a/.changeset/thirty-games-shake.md b/.changeset/thirty-games-shake.md deleted file mode 100644 index 7419098a01..0000000000 --- a/.changeset/thirty-games-shake.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@hyperlane-xyz/utils': minor -'@hyperlane-xyz/sdk': minor ---- - -Moved Hook/ISM config stringify into a general object stringify utility. diff --git a/.gitattributes b/.gitattributes index e0732994f9..d061a18bd0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ typescript/sdk/src/cw-types/*.types.ts linguist-generated=true rust/chains/hyperlane-ethereum/abis/*.abi.json linguist-generated=true +solidity/contracts/interfaces/avs/*.sol linguist-vendored=true +solidity/contracts/avs/ECDSA*.sol linguist-vendored=true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5afe02b48b..4a4727d03c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,8 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: + - '*' # run against all branches # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -175,6 +176,7 @@ jobs: e2e-matrix: runs-on: larger-runner + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') needs: [yarn-build] strategy: matrix: @@ -264,6 +266,7 @@ jobs: cli-e2e: runs-on: larger-runner + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') needs: [yarn-build] strategy: matrix: @@ -343,9 +346,6 @@ jobs: - environment: testnet4 chain: sepolia module: core - - environment: mainnet3 - chain: inevm - module: warp steps: - uses: actions/checkout@v3 diff --git a/.gitmodules b/.gitmodules index 3077b9e20f..d5392fba8e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "solidity/lib/forge-std"] path = solidity/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "solidity/lib/fx-portal"] + path = solidity/lib/fx-portal + url = https://github.com/0xPolygon/fx-portal diff --git a/.husky/pre-commit b/.husky/pre-commit index ea48d6b17a..8eb8e9c845 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,6 +3,8 @@ yarn lint-staged +echo "📝 If you haven't yet, please add a changeset for your changes via 'yarn changeset'" + # if any *.rs files have changed if git diff --staged --exit-code --name-only | grep -q -E ".*\.rs$"; then echo "Running cargo fmt pre-commit hook" diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..9a2a0e219c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20 diff --git a/README.md b/README.md index d1767c3c37..2d2d770c65 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,25 @@ foundryup Check out the [Foundry Book](https://book.getfoundry.sh/getting-started/installation) for more information. +### Node + +This repository targets v20 of node. We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage your node version. + +To install nvm + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +``` + +To install version 20 + +```bash +nvm install 20 +nvm use 20 +``` + +You should change versions automatically with the `.nvmrc` file. + ### Workspaces This monorepo uses [Yarn Workspaces](https://yarnpkg.com/features/workspaces). Installing dependencies, building, testing, and running prettier for all packages can be done from the root directory of the repository. diff --git a/package.json b/package.json index 344b1f87d1..3da986a519 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prettier": "yarn workspaces foreach --since --parallel run prettier", "lint": "yarn workspaces foreach --all --parallel run lint", "test": "yarn workspaces foreach --all --parallel run test", - "test:ci": "yarn workspaces foreach --all --parallel run test:ci", + "test:ci": "yarn workspaces foreach --all --topological run test:ci", "coverage": "yarn workspaces foreach --all --parallel run coverage", "version:prepare": "yarn changeset version && yarn workspaces foreach --all --parallel run version:update && yarn install --no-immutable", "version:check": "yarn changeset status", diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f72d8580c4..d8b69f4ea4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2678,7 +2678,7 @@ dependencies = [ [[package]] name = "ethers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -2692,7 +2692,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "ethers-core", "once_cell", @@ -2703,7 +2703,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -2721,7 +2721,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "Inflector", "cfg-if", @@ -2745,7 +2745,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -2759,7 +2759,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "arrayvec", "bytes", @@ -2789,7 +2789,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "ethers-core", "getrandom 0.2.12", @@ -2805,7 +2805,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "async-trait", "auto_impl 0.5.0", @@ -2853,7 +2853,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "async-trait", "auto_impl 1.1.0", @@ -2889,7 +2889,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-11-29-02#c9ced035628da59376c369be035facda1648577a" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-04-25#361b69b9561e11eb3cf8000a51de1985e2571785" dependencies = [ "async-trait", "coins-bip32", @@ -4272,6 +4272,7 @@ dependencies = [ "ethers-core", "ethers-prometheus", "ethers-signers", + "eyre", "futures-util", "hex 0.4.3", "hyperlane-core", @@ -7184,6 +7185,9 @@ version = "0.1.0" dependencies = [ "cosmwasm-schema", "ctrlc", + "ethers", + "ethers-contract", + "ethers-core", "eyre", "hex 0.4.3", "hyperlane-core", @@ -7200,6 +7204,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tempfile", + "tokio", "toml_edit 0.19.15", "ureq", "which", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 94af0fbbc0..0322d76292 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -193,27 +193,27 @@ cosmwasm-schema = "1.2.7" [workspace.dependencies.ethers] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2023-11-29-02" +tag = "2024-04-25" [workspace.dependencies.ethers-contract] features = ["legacy"] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2023-11-29-02" +tag = "2024-04-25" [workspace.dependencies.ethers-core] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2023-11-29-02" +tag = "2024-04-25" [workspace.dependencies.ethers-providers] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2023-11-29-02" +tag = "2024-04-25" [workspace.dependencies.ethers-signers] features = ["aws"] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2023-11-29-02" +tag = "2024-04-25" [patch.crates-io.curve25519-dalek] branch = "v3.2.2-relax-zeroize" diff --git a/rust/agents/relayer/Cargo.toml b/rust/agents/relayer/Cargo.toml index 655b4aea89..59b1c74d6b 100644 --- a/rust/agents/relayer/Cargo.toml +++ b/rust/agents/relayer/Cargo.toml @@ -31,7 +31,7 @@ serde.workspace = true serde_json.workspace = true strum.workspace = true thiserror.workspace = true -tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] } +tokio = { workspace = true, features = ["rt", "macros", "parking_lot", "rt-multi-thread"] } tracing-futures.workspace = true tracing.workspace = true diff --git a/rust/agents/relayer/src/main.rs b/rust/agents/relayer/src/main.rs index 3b85652ab4..1223702f8b 100644 --- a/rust/agents/relayer/src/main.rs +++ b/rust/agents/relayer/src/main.rs @@ -21,7 +21,7 @@ mod relayer; mod server; mod settings; -#[tokio::main(flavor = "current_thread")] +#[tokio::main(flavor = "multi_thread", worker_threads = 20)] async fn main() -> Result<()> { agent_main::().await } diff --git a/rust/agents/relayer/src/msg/metadata/aggregation.rs b/rust/agents/relayer/src/msg/metadata/aggregation.rs index fed501b622..ec5301c72b 100644 --- a/rust/agents/relayer/src/msg/metadata/aggregation.rs +++ b/rust/agents/relayer/src/msg/metadata/aggregation.rs @@ -117,7 +117,7 @@ impl AggregationIsmMetadataBuilder { #[async_trait] impl MetadataBuilder for AggregationIsmMetadataBuilder { - #[instrument(err, skip(self))] + #[instrument(err, skip(self), ret)] async fn build( &self, ism_address: H256, diff --git a/rust/agents/relayer/src/msg/metadata/base.rs b/rust/agents/relayer/src/msg/metadata/base.rs index 9fb65902e6..74449d10ee 100644 --- a/rust/agents/relayer/src/msg/metadata/base.rs +++ b/rust/agents/relayer/src/msg/metadata/base.rs @@ -41,6 +41,7 @@ pub enum MetadataBuilderError { MaxDepthExceeded(u32), } +#[derive(Debug)] pub struct IsmWithMetadataAndType { pub ism: Box, pub metadata: Option>, @@ -224,7 +225,7 @@ impl MessageMetadataBuilder { } } - #[instrument(err, skip(self), fields(destination_domain=self.destination_domain().name()))] + #[instrument(err, skip(self), fields(destination_domain=self.destination_domain().name()), ret)] pub async fn build_ism_and_metadata( &self, ism_address: H256, diff --git a/rust/agents/relayer/src/msg/metadata/routing.rs b/rust/agents/relayer/src/msg/metadata/routing.rs index c51cd69baf..c16fbc2a2d 100644 --- a/rust/agents/relayer/src/msg/metadata/routing.rs +++ b/rust/agents/relayer/src/msg/metadata/routing.rs @@ -14,7 +14,7 @@ pub struct RoutingIsmMetadataBuilder { #[async_trait] impl MetadataBuilder for RoutingIsmMetadataBuilder { - #[instrument(err, skip(self))] + #[instrument(err, skip(self), ret)] async fn build( &self, ism_address: H256, diff --git a/rust/agents/relayer/src/msg/mod.rs b/rust/agents/relayer/src/msg/mod.rs index 91d1104496..60c2ce0c56 100644 --- a/rust/agents/relayer/src/msg/mod.rs +++ b/rust/agents/relayer/src/msg/mod.rs @@ -28,7 +28,7 @@ pub(crate) mod gas_payment; pub(crate) mod metadata; pub(crate) mod op_queue; +pub(crate) mod op_submitter; pub(crate) mod pending_message; pub(crate) mod pending_operation; pub(crate) mod processor; -pub(crate) mod serial_submitter; diff --git a/rust/agents/relayer/src/msg/op_queue.rs b/rust/agents/relayer/src/msg/op_queue.rs index b23c54ab26..ef8c2ad2d3 100644 --- a/rust/agents/relayer/src/msg/op_queue.rs +++ b/rust/agents/relayer/src/msg/op_queue.rs @@ -4,7 +4,7 @@ use derive_new::new; use hyperlane_core::MpmcReceiver; use prometheus::{IntGauge, IntGaugeVec}; use tokio::sync::Mutex; -use tracing::info; +use tracing::{info, instrument}; use crate::server::MessageRetryRequest; @@ -25,6 +25,7 @@ pub struct OpQueue { impl OpQueue { /// Push an element onto the queue and update metrics + #[instrument(skip(self), ret, fields(queue_label=%self.queue_metrics_label), level = "debug")] pub async fn push(&self, op: QueueOperation) { // increment the metric before pushing onto the queue, because we lose ownership afterwards self.get_operation_metric(op.as_ref()).inc(); @@ -33,15 +34,28 @@ impl OpQueue { } /// Pop an element from the queue and update metrics - pub async fn pop(&mut self) -> Option> { + #[instrument(skip(self), ret, fields(queue_label=%self.queue_metrics_label), level = "debug")] + pub async fn pop(&mut self) -> Option { + let pop_attempt = self.pop_many(1).await; + pop_attempt.into_iter().next() + } + + /// Pop multiple elements at once from the queue and update metrics + #[instrument(skip(self), ret, fields(queue_label=%self.queue_metrics_label), level = "debug")] + pub async fn pop_many(&mut self, limit: usize) -> Vec { self.process_retry_requests().await; - let op = self.queue.lock().await.pop(); - op.map(|op| { + let mut queue = self.queue.lock().await; + let mut popped = vec![]; + while let Some(Reverse(op)) = queue.pop() { // even if the metric is decremented here, the operation may fail to process and be re-added to the queue. - // in those cases, the queue length will decrease to zero until the operation is re-added. - self.get_operation_metric(op.0.as_ref()).dec(); - op - }) + // in those cases, the queue length will look like it has spikes whose sizes are at most `limit` + self.get_operation_metric(op.as_ref()).dec(); + popped.push(op); + if popped.len() >= limit { + break; + } + } + popped } pub async fn process_retry_requests(&mut self) { @@ -59,18 +73,18 @@ impl OpQueue { let mut queue = self.queue.lock().await; let mut reprioritized_queue: BinaryHeap<_> = queue .drain() - .map(|Reverse(mut e)| { + .map(|Reverse(mut op)| { // Can check for equality here because of the PartialEq implementation for MessageRetryRequest, // but can't use `contains` because the types are different - if message_retry_requests.iter().any(|r| r == e) { - let destination_domain = e.destination_domain().to_string(); + if message_retry_requests.iter().any(|r| r == op) { info!( - id = ?e.id(), - destination_domain, "Retrying OpQueue operation" + operation = %op, + queue_label = %self.queue_metrics_label, + "Retrying OpQueue operation" ); - e.reset_attempts() + op.reset_attempts() } - Reverse(e) + Reverse(op) }) .collect(); queue.append(&mut reprioritized_queue); @@ -88,7 +102,10 @@ impl OpQueue { mod test { use super::*; use crate::msg::pending_operation::PendingOperationResult; - use hyperlane_core::{HyperlaneDomain, KnownHyperlaneDomain, MpmcChannel, H256}; + use hyperlane_core::{ + HyperlaneDomain, HyperlaneMessage, KnownHyperlaneDomain, MpmcChannel, TryBatchAs, + TxOutcome, H256, + }; use std::{ collections::VecDeque, time::{Duration, Instant}, @@ -111,6 +128,8 @@ mod test { } } + impl TryBatchAs for MockPendingOperation {} + #[async_trait::async_trait] impl PendingOperation for MockPendingOperation { fn id(&self) -> H256 { @@ -147,7 +166,11 @@ mod test { /// Submit this operation to the blockchain and report if it was successful /// or not. - async fn submit(&mut self) -> PendingOperationResult { + async fn submit(&mut self) { + todo!() + } + + fn set_submission_outcome(&mut self, _outcome: TxOutcome) { todo!() } @@ -166,6 +189,10 @@ mod test { ) } + fn set_next_attempt_after(&mut self, _delay: Duration) { + todo!() + } + fn set_retries(&mut self, _retries: u32) { todo!() } @@ -228,7 +255,7 @@ mod test { // Pop elements from queue 1 let mut queue_1_popped = vec![]; while let Some(op) = op_queue_1.pop().await { - queue_1_popped.push(op.0); + queue_1_popped.push(op); } // The elements sent over the channel should be the first ones popped, @@ -240,7 +267,7 @@ mod test { // Pop elements from queue 2 let mut queue_2_popped = vec![]; while let Some(op) = op_queue_2.pop().await { - queue_2_popped.push(op.0); + queue_2_popped.push(op); } // The elements should be popped in the order they were pushed, because there was no retry request for them @@ -287,7 +314,7 @@ mod test { // Pop elements from queue let mut popped = vec![]; while let Some(op) = op_queue.pop().await { - popped.push(op.0.id()); + popped.push(op.id()); } // First messages should be those to `destination_domain_2` - their exact order depends on diff --git a/rust/agents/relayer/src/msg/op_submitter.rs b/rust/agents/relayer/src/msg/op_submitter.rs new file mode 100644 index 0000000000..4350baeff2 --- /dev/null +++ b/rust/agents/relayer/src/msg/op_submitter.rs @@ -0,0 +1,459 @@ +use std::time::Duration; + +use derive_new::new; +use futures::future::join_all; +use futures_util::future::try_join_all; +use prometheus::{IntCounter, IntGaugeVec}; +use tokio::spawn; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tokio::time::sleep; +use tracing::{debug, info_span, instrument, instrument::Instrumented, trace, Instrument}; +use tracing::{info, warn}; + +use hyperlane_base::CoreMetrics; +use hyperlane_core::{ + BatchItem, ChainCommunicationError, ChainResult, HyperlaneDomain, HyperlaneDomainProtocol, + HyperlaneMessage, MpmcReceiver, TxOutcome, +}; + +use crate::msg::pending_message::CONFIRM_DELAY; +use crate::server::MessageRetryRequest; + +use super::op_queue::{OpQueue, QueueOperation}; +use super::pending_operation::*; + +/// SerialSubmitter accepts operations over a channel. It is responsible for +/// executing the right strategy to deliver those messages to the destination +/// chain. It is designed to be used in a scenario allowing only one +/// simultaneously in-flight submission, a consequence imposed by strictly +/// ordered nonces at the target chain combined with a hesitancy to +/// speculatively batch > 1 messages with a sequence of nonces, which entails +/// harder to manage error recovery, could lead to head of line blocking, etc. +/// +/// The single transaction execution slot is (likely) a bottlenecked resource +/// under steady state traffic, so the SerialSubmitter implemented in this file +/// carefully schedules work items onto the constrained +/// resource (transaction execution slot) according to a policy that +/// incorporates both user-visible metrics and message operation readiness +/// checks. +/// +/// Operations which failed processing due to a retriable error are also +/// retained within the SerialSubmitter, and will eventually be retried +/// according to our prioritization rule. +/// +/// Finally, the SerialSubmitter ensures that message delivery is robust to +/// destination chain reorgs prior to committing delivery status to +/// HyperlaneRocksDB. +/// +/// +/// Objectives +/// ---------- +/// +/// A few primary objectives determine the structure of this scheduler: +/// +/// 1. Progress for well-behaved applications should not be inhibited by +/// delivery of messages for which we have evidence of possible issues +/// (i.e., that we have already tried and failed to deliver them, and have +/// retained them for retry). So we should attempt processing operations +/// (num_retries=0) before ones that have been failing for a +/// while (num_retries>0) +/// +/// 2. Operations should be executed in in-order, i.e. if op_a was sent on +/// source chain prior to op_b, and they're both destined for the same +/// destination chain and are otherwise eligible, we should try to deliver op_a +/// before op_b, all else equal. This is because we expect applications may +/// prefer this even if they do not strictly rely on it for correctness. +/// +/// 3. Be [work-conserving](https://en.wikipedia.org/wiki/Work-conserving_scheduler) w.r.t. +/// the single execution slot, i.e. so long as there is at least one message +/// eligible for submission, we should be working on it within reason. This +/// must be balanced with the cost of making RPCs that will almost certainly +/// fail and potentially block new messages from being sent immediately. +#[derive(Debug, new)] +pub struct SerialSubmitter { + /// Domain this submitter delivers to. + domain: HyperlaneDomain, + /// Receiver for new messages to submit. + rx: mpsc::UnboundedReceiver, + /// Receiver for retry requests. + retry_rx: MpmcReceiver, + /// Metrics for serial submitter. + metrics: SerialSubmitterMetrics, + /// Max batch size for submitting messages + max_batch_size: u32, +} + +impl SerialSubmitter { + pub fn spawn(self) -> Instrumented> { + let span = info_span!("SerialSubmitter", destination=%self.domain); + spawn(async move { self.run().await }).instrument(span) + } + + async fn run(self) { + let Self { + domain, + metrics, + rx: rx_prepare, + retry_rx, + max_batch_size, + } = self; + let prepare_queue = OpQueue::new( + metrics.submitter_queue_length.clone(), + "prepare_queue".to_string(), + retry_rx.clone(), + ); + let submit_queue = OpQueue::new( + metrics.submitter_queue_length.clone(), + "submit_queue".to_string(), + retry_rx.clone(), + ); + let confirm_queue = OpQueue::new( + metrics.submitter_queue_length.clone(), + "confirm_queue".to_string(), + retry_rx, + ); + + let tasks = [ + spawn(receive_task( + domain.clone(), + rx_prepare, + prepare_queue.clone(), + )), + spawn(prepare_task( + domain.clone(), + prepare_queue.clone(), + submit_queue.clone(), + confirm_queue.clone(), + max_batch_size, + metrics.clone(), + )), + spawn(submit_task( + domain.clone(), + submit_queue, + confirm_queue.clone(), + max_batch_size, + metrics.clone(), + )), + spawn(confirm_task( + domain.clone(), + prepare_queue, + confirm_queue, + max_batch_size, + metrics, + )), + ]; + + if let Err(err) = try_join_all(tasks).await { + tracing::error!( + error=?err, + ?domain, + "SerialSubmitter task panicked for domain" + ); + } + } +} + +#[instrument(skip_all, fields(%domain))] +async fn receive_task( + domain: HyperlaneDomain, + mut rx: mpsc::UnboundedReceiver, + prepare_queue: OpQueue, +) { + // Pull any messages sent to this submitter + while let Some(op) = rx.recv().await { + trace!(?op, "Received new operation"); + // make sure things are getting wired up correctly; if this works in testing it + // should also be valid in production. + debug_assert_eq!(*op.destination_domain(), domain); + prepare_queue.push(op).await; + } +} + +#[instrument(skip_all, fields(%domain))] +async fn prepare_task( + domain: HyperlaneDomain, + mut prepare_queue: OpQueue, + submit_queue: OpQueue, + confirm_queue: OpQueue, + max_batch_size: u32, + metrics: SerialSubmitterMetrics, +) { + // Prepare at most `max_batch_size` ops at a time to avoid getting rate-limited + let ops_to_prepare = max_batch_size as usize; + loop { + // Pop messages here according to the configured batch. + let mut batch = prepare_queue.pop_many(ops_to_prepare).await; + if batch.is_empty() { + // queue is empty so give some time before checking again to prevent burning CPU + sleep(Duration::from_millis(100)).await; + continue; + } + let mut task_prep_futures = vec![]; + let op_refs = batch.iter_mut().map(|op| op.as_mut()).collect::>(); + for op in op_refs { + trace!(?op, "Preparing operation"); + debug_assert_eq!(*op.destination_domain(), domain); + task_prep_futures.push(op.prepare()); + } + let res = join_all(task_prep_futures).await; + let not_ready_count = res + .iter() + .filter(|r| { + matches!( + r, + PendingOperationResult::NotReady | PendingOperationResult::Reprepare + ) + }) + .count(); + let batch_len = batch.len(); + for (op, prepare_result) in batch.into_iter().zip(res.into_iter()) { + match prepare_result { + PendingOperationResult::Success => { + debug!(?op, "Operation prepared"); + metrics.ops_prepared.inc(); + // TODO: push multiple messages at once + submit_queue.push(op).await; + } + PendingOperationResult::NotReady => { + prepare_queue.push(op).await; + } + PendingOperationResult::Reprepare => { + metrics.ops_failed.inc(); + prepare_queue.push(op).await; + } + PendingOperationResult::Drop => { + metrics.ops_dropped.inc(); + } + PendingOperationResult::Confirm => { + confirm_queue.push(op).await; + } + } + } + if not_ready_count == batch_len { + // none of the operations are ready yet, so wait for a little bit + sleep(Duration::from_millis(500)).await; + } + } +} + +#[instrument(skip_all, fields(%domain))] +async fn submit_task( + domain: HyperlaneDomain, + mut submit_queue: OpQueue, + mut confirm_queue: OpQueue, + max_batch_size: u32, + metrics: SerialSubmitterMetrics, +) { + let recv_limit = max_batch_size as usize; + loop { + let mut batch = submit_queue.pop_many(recv_limit).await; + + match batch.len().cmp(&1) { + std::cmp::Ordering::Less => { + // The queue is empty, so give some time before checking again to prevent burning CPU + sleep(Duration::from_millis(100)).await; + continue; + } + std::cmp::Ordering::Equal => { + let op = batch.pop().unwrap(); + submit_single_operation(op, &mut confirm_queue, &metrics).await; + } + std::cmp::Ordering::Greater => { + OperationBatch::new(batch, domain.clone()) + .submit(&mut confirm_queue, &metrics) + .await; + } + } + } +} + +#[instrument(skip(confirm_queue, metrics), ret, level = "debug")] +async fn submit_single_operation( + mut op: QueueOperation, + confirm_queue: &mut OpQueue, + metrics: &SerialSubmitterMetrics, +) { + let destination = op.destination_domain().clone(); + op.submit().await; + debug!(?op, "Operation submitted"); + op.set_next_attempt_after(CONFIRM_DELAY); + confirm_queue.push(op).await; + metrics.ops_submitted.inc(); + + if matches!( + destination.domain_protocol(), + HyperlaneDomainProtocol::Cosmos + ) { + // On cosmos chains, sleep for 1 sec (the finality period). + // Otherwise we get `account sequence mismatch` errors, which have caused us + // to lose liveness. + sleep(Duration::from_secs(1)).await; + } +} + +#[instrument(skip_all, fields(%domain))] +async fn confirm_task( + domain: HyperlaneDomain, + prepare_queue: OpQueue, + mut confirm_queue: OpQueue, + max_batch_size: u32, + metrics: SerialSubmitterMetrics, +) { + let recv_limit = max_batch_size as usize; + loop { + // Pick the next message to try confirming. + let batch = confirm_queue.pop_many(recv_limit).await; + + if batch.is_empty() { + // queue is empty so give some time before checking again to prevent burning CPU + sleep(Duration::from_millis(200)).await; + continue; + } + + let futures = batch.into_iter().map(|op| { + confirm_operation( + op, + domain.clone(), + prepare_queue.clone(), + confirm_queue.clone(), + metrics.clone(), + ) + }); + let op_results = join_all(futures).await; + if op_results.iter().all(|op| { + matches!( + op, + PendingOperationResult::NotReady | PendingOperationResult::Confirm + ) + }) { + // None of the operations are ready, so wait for a little bit + // before checking again to prevent burning CPU + sleep(Duration::from_millis(500)).await; + } + } +} + +async fn confirm_operation( + mut op: QueueOperation, + domain: HyperlaneDomain, + prepare_queue: OpQueue, + confirm_queue: OpQueue, + metrics: SerialSubmitterMetrics, +) -> PendingOperationResult { + trace!(?op, "Confirming operation"); + debug_assert_eq!(*op.destination_domain(), domain); + + let operation_result = op.confirm().await; + match operation_result { + PendingOperationResult::Success => { + debug!(?op, "Operation confirmed"); + metrics.ops_confirmed.inc(); + } + PendingOperationResult::NotReady | PendingOperationResult::Confirm => { + // TODO: push multiple messages at once + confirm_queue.push(op).await; + } + PendingOperationResult::Reprepare => { + metrics.ops_failed.inc(); + prepare_queue.push(op).await; + } + PendingOperationResult::Drop => { + metrics.ops_dropped.inc(); + } + } + operation_result +} + +#[derive(Debug, Clone)] +pub struct SerialSubmitterMetrics { + submitter_queue_length: IntGaugeVec, + ops_prepared: IntCounter, + ops_submitted: IntCounter, + ops_confirmed: IntCounter, + ops_failed: IntCounter, + ops_dropped: IntCounter, +} + +impl SerialSubmitterMetrics { + pub fn new(metrics: &CoreMetrics, destination: &HyperlaneDomain) -> Self { + let destination = destination.name(); + Self { + submitter_queue_length: metrics.submitter_queue_length(), + ops_prepared: metrics + .operations_processed_count() + .with_label_values(&["prepared", destination]), + ops_submitted: metrics + .operations_processed_count() + .with_label_values(&["submitted", destination]), + ops_confirmed: metrics + .operations_processed_count() + .with_label_values(&["confirmed", destination]), + ops_failed: metrics + .operations_processed_count() + .with_label_values(&["failed", destination]), + ops_dropped: metrics + .operations_processed_count() + .with_label_values(&["dropped", destination]), + } + } +} + +#[derive(new, Debug)] +struct OperationBatch { + operations: Vec, + #[allow(dead_code)] + domain: HyperlaneDomain, +} + +impl OperationBatch { + async fn submit(self, confirm_queue: &mut OpQueue, metrics: &SerialSubmitterMetrics) { + match self.try_submit_as_batch(metrics).await { + Ok(outcome) => { + // TODO: use the `tx_outcome` with the total gas expenditure + // We'll need to proportionally set `used_gas` based on the tx_outcome, so it can be updated in the confirm step + // which means we need to add a `set_transaction_outcome` fn to `PendingOperation` + info!(outcome=?outcome, batch_size=self.operations.len(), batch=?self.operations, "Submitted transaction batch"); + for mut op in self.operations { + op.set_next_attempt_after(CONFIRM_DELAY); + confirm_queue.push(op).await; + } + return; + } + Err(e) => { + warn!(error=?e, batch=?self.operations, "Error when submitting batch. Falling back to serial submission."); + } + } + self.submit_serially(confirm_queue, metrics).await; + } + + #[instrument(skip(metrics), ret, level = "debug")] + async fn try_submit_as_batch( + &self, + metrics: &SerialSubmitterMetrics, + ) -> ChainResult { + let batch = self + .operations + .iter() + .map(|op| op.try_batch()) + .collect::>>>()?; + + // We already assume that the relayer submits to a single mailbox per destination. + // So it's fine to use the first item in the batch to get the mailbox. + let Some(first_item) = batch.first() else { + return Err(ChainCommunicationError::BatchIsEmpty); + }; + + // We use the estimated gas limit from the prior call to + // `process_estimate_costs` to avoid a second gas estimation. + let outcome = first_item.mailbox.process_batch(&batch).await?; + metrics.ops_submitted.inc_by(self.operations.len() as u64); + Ok(outcome) + } + + async fn submit_serially(self, confirm_queue: &mut OpQueue, metrics: &SerialSubmitterMetrics) { + for op in self.operations.into_iter() { + submit_single_operation(op, confirm_queue, metrics).await; + } + } +} diff --git a/rust/agents/relayer/src/msg/pending_message.rs b/rust/agents/relayer/src/msg/pending_message.rs index 751fe299fb..1bfabeded1 100644 --- a/rust/agents/relayer/src/msg/pending_message.rs +++ b/rust/agents/relayer/src/msg/pending_message.rs @@ -8,7 +8,10 @@ use async_trait::async_trait; use derive_new::new; use eyre::Result; use hyperlane_base::{db::HyperlaneRocksDB, CoreMetrics}; -use hyperlane_core::{HyperlaneChain, HyperlaneDomain, HyperlaneMessage, Mailbox, H256, U256}; +use hyperlane_core::{ + BatchItem, ChainCommunicationError, ChainResult, HyperlaneChain, HyperlaneDomain, + HyperlaneMessage, Mailbox, MessageSubmissionData, TryBatchAs, TxOutcome, H256, U256, +}; use prometheus::{IntCounter, IntGauge}; use tracing::{debug, error, info, instrument, trace, warn}; @@ -18,12 +21,12 @@ use super::{ pending_operation::*, }; -const CONFIRM_DELAY: Duration = if cfg!(any(test, feature = "test-utils")) { +pub const CONFIRM_DELAY: Duration = if cfg!(any(test, feature = "test-utils")) { // Wait 5 seconds after submitting the message before confirming in test mode Duration::from_secs(5) } else { - // Wait 10 min after submitting the message before confirming in normal/production mode - Duration::from_secs(60 * 10) + // Wait 1 min after submitting the message before confirming in normal/production mode + Duration::from_secs(60) }; /// The message context contains the links needed to submit a message. Each @@ -54,19 +57,15 @@ pub struct PendingMessage { #[new(default)] submitted: bool, #[new(default)] - submission_data: Option>, + submission_data: Option>, #[new(default)] num_retries: u32, #[new(value = "Instant::now()")] last_attempted_at: Instant, #[new(default)] next_attempt_after: Option, -} - -/// State for the next submission attempt generated by a prepare call. -struct SubmissionData { - metadata: Vec, - gas_limit: U256, + #[new(default)] + submission_outcome: Option, } impl Debug for PendingMessage { @@ -99,6 +98,22 @@ impl PartialEq for PendingMessage { impl Eq for PendingMessage {} +impl TryBatchAs for PendingMessage { + fn try_batch(&self) -> ChainResult> { + match self.submission_data.as_ref() { + None => { + warn!("Cannot batch message without submission data, returning BatchingFailed"); + Err(ChainCommunicationError::BatchingFailed) + } + Some(data) => Ok(BatchItem::new( + self.message.clone(), + data.as_ref().clone(), + self.ctx.destination_mailbox.clone(), + )), + } + } +} + #[async_trait] impl PendingOperation for PendingMessage { fn id(&self) -> H256 { @@ -121,7 +136,7 @@ impl PendingOperation for PendingMessage { self.app_context.clone() } - #[instrument] + #[instrument(skip(self), ret, fields(id=%self.id()), level = "debug")] async fn prepare(&mut self) -> PendingOperationResult { make_op_try!(|| self.on_reprepare()); @@ -143,8 +158,8 @@ impl PendingOperation for PendingMessage { if is_already_delivered { debug!("Message has already been delivered, marking as submitted."); self.submitted = true; - self.next_attempt_after = Some(Instant::now() + CONFIRM_DELAY); - return PendingOperationResult::Success; + self.set_next_attempt_after(CONFIRM_DELAY); + return PendingOperationResult::Confirm; } let provider = self.ctx.destination_mailbox.provider(); @@ -229,7 +244,7 @@ impl PendingOperation for PendingMessage { } } - self.submission_data = Some(Box::new(SubmissionData { + self.submission_data = Some(Box::new(MessageSubmissionData { metadata, gas_limit, })); @@ -237,18 +252,12 @@ impl PendingOperation for PendingMessage { } #[instrument] - async fn submit(&mut self) -> PendingOperationResult { - make_op_try!(|| self.on_reprepare()); - + async fn submit(&mut self) { if self.submitted { // this message has already been submitted, possibly not by us - return PendingOperationResult::Success; + return; } - // skip checking `is_ready` here because the definition of ready is it having - // been prepared successfully and we don't want to introduce any delay into the - // submission process. - let state = self .submission_data .take() @@ -256,33 +265,25 @@ impl PendingOperation for PendingMessage { // We use the estimated gas limit from the prior call to // `process_estimate_costs` to avoid a second gas estimation. - let tx_outcome = op_try!( - self.ctx - .destination_mailbox - .process(&self.message, &state.metadata, Some(state.gas_limit)) - .await, - "processing message" - ); - - op_try!(critical: self.ctx.origin_gas_payment_enforcer.record_tx_outcome(&self.message, tx_outcome.clone()), "recording tx outcome"); - if tx_outcome.executed { - info!( - txid=?tx_outcome.transaction_id, - "Message successfully processed by transaction" - ); - self.submitted = true; - self.reset_attempts(); - self.next_attempt_after = Some(Instant::now() + CONFIRM_DELAY); - PendingOperationResult::Success - } else { - warn!( - txid=?tx_outcome.transaction_id, - "Transaction attempting to process message reverted" - ); - self.on_reprepare() + let tx_outcome = self + .ctx + .destination_mailbox + .process(&self.message, &state.metadata, Some(state.gas_limit)) + .await; + match tx_outcome { + Ok(outcome) => { + self.set_submission_outcome(outcome); + } + Err(e) => { + error!(error=?e, "Error when processing message"); + } } } + fn set_submission_outcome(&mut self, outcome: TxOutcome) { + self.submission_outcome = Some(outcome); + } + async fn confirm(&mut self) -> PendingOperationResult { make_op_try!(|| { // Provider error; just try again later @@ -291,11 +292,6 @@ impl PendingOperation for PendingMessage { PendingOperationResult::NotReady }); - debug_assert!( - self.submitted, - "Confirm called before message was submitted" - ); - if !self.is_ready() { return PendingOperationResult::NotReady; } @@ -312,9 +308,26 @@ impl PendingOperation for PendingMessage { critical: self.record_message_process_success(), "recording message process success" ); + info!( + submission=?self.submission_outcome, + "Message successfully processed" + ); PendingOperationResult::Success } else { - self.reset_attempts(); + if let Some(outcome) = &self.submission_outcome { + if let Err(e) = self + .ctx + .origin_gas_payment_enforcer + .record_tx_outcome(&self.message, outcome.clone()) + { + error!(error=?e, "Error when recording tx outcome"); + } + } + warn!( + tx_outcome=?self.submission_outcome, + message_id=?self.message.id(), + "Transaction attempting to process message either reverted or was reorged" + ); self.on_reprepare() } } @@ -323,6 +336,10 @@ impl PendingOperation for PendingMessage { self.next_attempt_after } + fn set_next_attempt_after(&mut self, delay: Duration) { + self.next_attempt_after = Some(Instant::now() + delay); + } + fn reset_attempts(&mut self) { self.reset_attempts(); } diff --git a/rust/agents/relayer/src/msg/pending_operation.rs b/rust/agents/relayer/src/msg/pending_operation.rs index 3a41b59616..206e062e2a 100644 --- a/rust/agents/relayer/src/msg/pending_operation.rs +++ b/rust/agents/relayer/src/msg/pending_operation.rs @@ -1,7 +1,11 @@ -use std::{cmp::Ordering, fmt::Debug, time::Instant}; +use std::{ + cmp::Ordering, + fmt::{Debug, Display}, + time::{Duration, Instant}, +}; use async_trait::async_trait; -use hyperlane_core::{HyperlaneDomain, H256}; +use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, TryBatchAs, TxOutcome, H256}; use super::op_queue::QueueOperation; @@ -25,7 +29,7 @@ use super::op_queue::QueueOperation; /// responsible for checking if the operation has reached a point at which we /// consider it safe from reorgs. #[async_trait] -pub trait PendingOperation: Send + Sync + Debug { +pub trait PendingOperation: Send + Sync + Debug + TryBatchAs { /// Get the unique identifier for this operation. fn id(&self) -> H256; @@ -57,9 +61,11 @@ pub trait PendingOperation: Send + Sync + Debug { /// submit call. async fn prepare(&mut self) -> PendingOperationResult; - /// Submit this operation to the blockchain and report if it was successful - /// or not. - async fn submit(&mut self) -> PendingOperationResult; + /// Submit this operation to the blockchain + async fn submit(&mut self); + + /// Set the outcome of the `submit` call + fn set_submission_outcome(&mut self, outcome: TxOutcome); /// This will be called after the operation has been submitted and is /// responsible for checking if the operation has reached a point at @@ -72,6 +78,9 @@ pub trait PendingOperation: Send + Sync + Debug { /// returning `NotReady` if it is too early and matters. fn next_attempt_after(&self) -> Option; + /// Set the next time this operation should be attempted. + fn set_next_attempt_after(&mut self, delay: Duration); + /// Reset the number of attempts this operation has made, causing it to be /// retried immediately. fn reset_attempts(&mut self); @@ -81,6 +90,19 @@ pub trait PendingOperation: Send + Sync + Debug { fn set_retries(&mut self, retries: u32); } +impl Display for QueueOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "QueueOperation(id: {}, origin: {}, destination: {}, priority: {})", + self.id(), + self.origin_domain_id(), + self.destination_domain(), + self.priority() + ) + } +} + impl PartialOrd for QueueOperation { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -116,6 +138,7 @@ impl Ord for QueueOperation { } } +#[derive(Debug)] pub enum PendingOperationResult { /// Promote to the next step Success, @@ -125,6 +148,8 @@ pub enum PendingOperationResult { Reprepare, /// Do not attempt to run the operation again, forget about it Drop, + /// Send this message straight to the confirm queue + Confirm, } /// create a `op_try!` macro for the `on_retry` handler. diff --git a/rust/agents/relayer/src/msg/processor.rs b/rust/agents/relayer/src/msg/processor.rs index 072ec99d88..3aae0d308c 100644 --- a/rust/agents/relayer/src/msg/processor.rs +++ b/rust/agents/relayer/src/msg/processor.rs @@ -242,6 +242,7 @@ mod test { url: "http://example.com".parse().unwrap(), }, transaction_overrides: Default::default(), + operation_batch: Default::default(), }), metrics_conf: Default::default(), index: Default::default(), diff --git a/rust/agents/relayer/src/msg/serial_submitter.rs b/rust/agents/relayer/src/msg/serial_submitter.rs deleted file mode 100644 index b1bbc2c0a2..0000000000 --- a/rust/agents/relayer/src/msg/serial_submitter.rs +++ /dev/null @@ -1,314 +0,0 @@ -use std::cmp::Reverse; -use std::time::Duration; - -use derive_new::new; -use futures_util::future::try_join_all; -use prometheus::{IntCounter, IntGaugeVec}; -use tokio::spawn; -use tokio::sync::mpsc; -use tokio::task::JoinHandle; -use tokio::time::sleep; -use tracing::{debug, info_span, instrument, instrument::Instrumented, trace, Instrument}; - -use hyperlane_base::CoreMetrics; -use hyperlane_core::{HyperlaneDomain, MpmcReceiver}; - -use crate::server::MessageRetryRequest; - -use super::op_queue::{OpQueue, QueueOperation}; -use super::pending_operation::*; - -/// SerialSubmitter accepts operations over a channel. It is responsible for -/// executing the right strategy to deliver those messages to the destination -/// chain. It is designed to be used in a scenario allowing only one -/// simultaneously in-flight submission, a consequence imposed by strictly -/// ordered nonces at the target chain combined with a hesitancy to -/// speculatively batch > 1 messages with a sequence of nonces, which entails -/// harder to manage error recovery, could lead to head of line blocking, etc. -/// -/// The single transaction execution slot is (likely) a bottlenecked resource -/// under steady state traffic, so the SerialSubmitter implemented in this file -/// carefully schedules work items onto the constrained -/// resource (transaction execution slot) according to a policy that -/// incorporates both user-visible metrics and message operation readiness -/// checks. -/// -/// Operations which failed processing due to a retriable error are also -/// retained within the SerialSubmitter, and will eventually be retried -/// according to our prioritization rule. -/// -/// Finally, the SerialSubmitter ensures that message delivery is robust to -/// destination chain reorgs prior to committing delivery status to -/// HyperlaneRocksDB. -/// -/// -/// Objectives -/// ---------- -/// -/// A few primary objectives determine the structure of this scheduler: -/// -/// 1. Progress for well-behaved applications should not be inhibited by -/// delivery of messages for which we have evidence of possible issues -/// (i.e., that we have already tried and failed to deliver them, and have -/// retained them for retry). So we should attempt processing operations -/// (num_retries=0) before ones that have been failing for a -/// while (num_retries>0) -/// -/// 2. Operations should be executed in in-order, i.e. if op_a was sent on -/// source chain prior to op_b, and they're both destined for the same -/// destination chain and are otherwise eligible, we should try to deliver op_a -/// before op_b, all else equal. This is because we expect applications may -/// prefer this even if they do not strictly rely on it for correctness. -/// -/// 3. Be [work-conserving](https://en.wikipedia.org/wiki/Work-conserving_scheduler) w.r.t. -/// the single execution slot, i.e. so long as there is at least one message -/// eligible for submission, we should be working on it within reason. This -/// must be balanced with the cost of making RPCs that will almost certainly -/// fail and potentially block new messages from being sent immediately. -#[derive(Debug, new)] -pub struct SerialSubmitter { - /// Domain this submitter delivers to. - domain: HyperlaneDomain, - /// Receiver for new messages to submit. - rx: mpsc::UnboundedReceiver, - /// Receiver for retry requests. - retry_rx: MpmcReceiver, - /// Metrics for serial submitter. - metrics: SerialSubmitterMetrics, -} - -impl SerialSubmitter { - pub fn spawn(self) -> Instrumented> { - let span = info_span!("SerialSubmitter", destination=%self.domain); - spawn(async move { self.run().await }).instrument(span) - } - - async fn run(self) { - let Self { - domain, - metrics, - rx: rx_prepare, - retry_rx, - } = self; - let prepare_queue = OpQueue::new( - metrics.submitter_queue_length.clone(), - "prepare_queue".to_string(), - retry_rx.clone(), - ); - let confirm_queue = OpQueue::new( - metrics.submitter_queue_length.clone(), - "confirm_queue".to_string(), - retry_rx, - ); - - // This is a channel because we want to only have a small number of messages - // sitting ready to go at a time and this acts as a synchronization tool - // to slow down the preparation of messages when the submitter gets - // behind. - let (tx_submit, rx_submit) = mpsc::channel(1); - - let tasks = [ - spawn(receive_task( - domain.clone(), - rx_prepare, - prepare_queue.clone(), - )), - spawn(prepare_task( - domain.clone(), - prepare_queue.clone(), - tx_submit, - metrics.clone(), - )), - spawn(submit_task( - domain.clone(), - rx_submit, - prepare_queue.clone(), - confirm_queue.clone(), - metrics.clone(), - )), - spawn(confirm_task( - domain.clone(), - prepare_queue, - confirm_queue, - metrics, - )), - ]; - - if let Err(err) = try_join_all(tasks).await { - tracing::error!( - error=?err, - ?domain, - "SerialSubmitter task panicked for domain" - ); - } - } -} - -#[instrument(skip_all, fields(%domain))] -async fn receive_task( - domain: HyperlaneDomain, - mut rx: mpsc::UnboundedReceiver, - prepare_queue: OpQueue, -) { - // Pull any messages sent to this submitter - while let Some(op) = rx.recv().await { - trace!(?op, "Received new operation"); - // make sure things are getting wired up correctly; if this works in testing it - // should also be valid in production. - debug_assert_eq!(*op.destination_domain(), domain); - prepare_queue.push(op).await; - } -} - -#[instrument(skip_all, fields(%domain))] -async fn prepare_task( - domain: HyperlaneDomain, - mut prepare_queue: OpQueue, - tx_submit: mpsc::Sender, - metrics: SerialSubmitterMetrics, -) { - loop { - // Pick the next message to try preparing. - let next = prepare_queue.pop().await; - - let Some(Reverse(mut op)) = next else { - // queue is empty so give some time before checking again to prevent burning CPU - sleep(Duration::from_millis(200)).await; - continue; - }; - - trace!(?op, "Preparing operation"); - debug_assert_eq!(*op.destination_domain(), domain); - - match op.prepare().await { - PendingOperationResult::Success => { - debug!(?op, "Operation prepared"); - metrics.ops_prepared.inc(); - // this send will pause this task if the submitter is not ready to accept yet - if let Err(err) = tx_submit.send(op).await { - tracing::error!(error=?err, "Failed to send prepared operation to submitter"); - } - } - PendingOperationResult::NotReady => { - // none of the operations are ready yet, so wait for a little bit - prepare_queue.push(op).await; - sleep(Duration::from_millis(200)).await; - } - PendingOperationResult::Reprepare => { - metrics.ops_failed.inc(); - prepare_queue.push(op).await; - } - PendingOperationResult::Drop => { - metrics.ops_dropped.inc(); - } - } - } -} - -#[instrument(skip_all, fields(%domain))] -async fn submit_task( - domain: HyperlaneDomain, - mut rx_submit: mpsc::Receiver, - prepare_queue: OpQueue, - confirm_queue: OpQueue, - metrics: SerialSubmitterMetrics, -) { - while let Some(mut op) = rx_submit.recv().await { - trace!(?op, "Submitting operation"); - debug_assert_eq!(*op.destination_domain(), domain); - - match op.submit().await { - PendingOperationResult::Success => { - debug!(?op, "Operation submitted"); - metrics.ops_submitted.inc(); - confirm_queue.push(op).await; - } - PendingOperationResult::NotReady => { - panic!("Pending operation was prepared and therefore must be ready") - } - PendingOperationResult::Reprepare => { - metrics.ops_failed.inc(); - prepare_queue.push(op).await; - } - PendingOperationResult::Drop => { - metrics.ops_dropped.inc(); - } - } - } -} - -#[instrument(skip_all, fields(%domain))] -async fn confirm_task( - domain: HyperlaneDomain, - prepare_queue: OpQueue, - mut confirm_queue: OpQueue, - metrics: SerialSubmitterMetrics, -) { - loop { - // Pick the next message to try confirming. - let Some(Reverse(mut op)) = confirm_queue.pop().await else { - sleep(Duration::from_secs(5)).await; - continue; - }; - - trace!(?op, "Confirming operation"); - debug_assert_eq!(*op.destination_domain(), domain); - - match op.confirm().await { - PendingOperationResult::Success => { - debug!(?op, "Operation confirmed"); - metrics.ops_confirmed.inc(); - } - PendingOperationResult::NotReady => { - // none of the operations are ready yet, so wait for a little bit - confirm_queue.push(op).await; - sleep(Duration::from_secs(5)).await; - } - PendingOperationResult::Reprepare => { - metrics.ops_reorged.inc(); - prepare_queue.push(op).await; - } - PendingOperationResult::Drop => { - metrics.ops_dropped.inc(); - } - } - } -} - -#[derive(Debug, Clone)] -pub struct SerialSubmitterMetrics { - submitter_queue_length: IntGaugeVec, - ops_prepared: IntCounter, - ops_submitted: IntCounter, - ops_confirmed: IntCounter, - ops_reorged: IntCounter, - ops_failed: IntCounter, - ops_dropped: IntCounter, -} - -impl SerialSubmitterMetrics { - pub fn new(metrics: &CoreMetrics, destination: &HyperlaneDomain) -> Self { - let destination = destination.name(); - Self { - submitter_queue_length: metrics.submitter_queue_length(), - ops_prepared: metrics - .operations_processed_count() - .with_label_values(&["prepared", destination]), - ops_submitted: metrics - .operations_processed_count() - .with_label_values(&["submitted", destination]), - ops_confirmed: metrics - .operations_processed_count() - .with_label_values(&["confirmed", destination]), - ops_reorged: metrics - .operations_processed_count() - .with_label_values(&["reorged", destination]), - ops_failed: metrics - .operations_processed_count() - .with_label_values(&["failed", destination]), - ops_dropped: metrics - .operations_processed_count() - .with_label_values(&["dropped", destination]), - } - } -} diff --git a/rust/agents/relayer/src/relayer.rs b/rust/agents/relayer/src/relayer.rs index b0fae81421..581620f2f7 100644 --- a/rust/agents/relayer/src/relayer.rs +++ b/rust/agents/relayer/src/relayer.rs @@ -33,9 +33,9 @@ use crate::{ gas_payment::GasPaymentEnforcer, metadata::{BaseMetadataBuilder, IsmAwareAppContextClassifier}, op_queue::QueueOperation, + op_submitter::{SerialSubmitter, SerialSubmitterMetrics}, pending_message::{MessageContext, MessageSubmissionMetrics}, processor::{MessageProcessor, MessageProcessorMetrics}, - serial_submitter::{SerialSubmitter, SerialSubmitterMetrics}, }, server::{self as relayer_server, MessageRetryRequest}, settings::{matching_list::MatchingList, RelayerSettings}, @@ -307,11 +307,19 @@ impl BaseAgent for Relayer { let (send_channel, receive_channel) = mpsc::unbounded_channel::(); send_channels.insert(dest_domain.id(), send_channel); - tasks.push(self.run_destination_submitter( - dest_domain, - receive_channel, - mpmc_channel.receiver(), - )); + tasks.push( + self.run_destination_submitter( + dest_domain, + receive_channel, + mpmc_channel.receiver(), + // Default to submitting one message at a time if there is no batch config + self.core.settings.chains[dest_domain.name()] + .connection + .operation_batch_config() + .map(|c| c.max_batch_size) + .unwrap_or(1), + ), + ); let metrics_updater = MetricsUpdater::new( dest_conf, @@ -357,7 +365,7 @@ impl Relayer { .sync("dispatched_messages", cursor) .await }) - .instrument(info_span!("ContractSync")) + .instrument(info_span!("MessageSync")) } async fn run_interchain_gas_payment_sync( @@ -372,7 +380,7 @@ impl Relayer { .clone(); let cursor = contract_sync.cursor(index_settings).await; tokio::spawn(async move { contract_sync.clone().sync("gas_payments", cursor).await }) - .instrument(info_span!("ContractSync")) + .instrument(info_span!("IgpSync")) } async fn run_merkle_tree_hook_syncs( @@ -383,7 +391,7 @@ impl Relayer { let contract_sync = self.merkle_tree_hook_syncs.get(origin).unwrap().clone(); let cursor = contract_sync.cursor(index_settings).await; tokio::spawn(async move { contract_sync.clone().sync("merkle_tree_hook", cursor).await }) - .instrument(info_span!("ContractSync")) + .instrument(info_span!("MerkleTreeHookSync")) } fn run_message_processor( @@ -448,12 +456,14 @@ impl Relayer { destination: &HyperlaneDomain, receiver: UnboundedReceiver, retry_receiver_channel: MpmcReceiver, + batch_size: u32, ) -> Instrumented> { let serial_submitter = SerialSubmitter::new( destination.clone(), receiver, retry_receiver_channel, SerialSubmitterMetrics::new(&self.core.metrics, destination), + batch_size, ); let span = info_span!("SerialSubmitter", destination=%destination); let destination = destination.clone(); diff --git a/rust/agents/scraper/src/chain_scraper/mod.rs b/rust/agents/scraper/src/chain_scraper/mod.rs index 4240115d53..813f11967a 100644 --- a/rust/agents/scraper/src/chain_scraper/mod.rs +++ b/rust/agents/scraper/src/chain_scraper/mod.rs @@ -9,7 +9,7 @@ use hyperlane_base::settings::IndexSettings; use hyperlane_core::{ unwrap_or_none_result, BlockInfo, Delivery, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneProvider, HyperlaneSequenceAwareIndexerStoreReader, - HyperlaneWatermarkedLogStore, InterchainGasPayment, LogMeta, H256, + HyperlaneWatermarkedLogStore, Indexed, InterchainGasPayment, LogMeta, H256, }; use itertools::Itertools; use tracing::trace; @@ -269,7 +269,7 @@ impl HyperlaneSqlDb { #[async_trait] impl HyperlaneLogStore for HyperlaneSqlDb { /// Store messages from the origin mailbox into the database. - async fn store_logs(&self, messages: &[(HyperlaneMessage, LogMeta)]) -> Result { + async fn store_logs(&self, messages: &[(Indexed, LogMeta)]) -> Result { if messages.is_empty() { return Ok(0); } @@ -287,7 +287,7 @@ impl HyperlaneLogStore for HyperlaneSqlDb { ) .unwrap(); StorableMessage { - msg: m.0.clone(), + msg: m.0.inner().clone(), meta: &m.1, txn_id: txn.id, } @@ -302,7 +302,7 @@ impl HyperlaneLogStore for HyperlaneSqlDb { #[async_trait] impl HyperlaneLogStore for HyperlaneSqlDb { - async fn store_logs(&self, deliveries: &[(Delivery, LogMeta)]) -> Result { + async fn store_logs(&self, deliveries: &[(Indexed, LogMeta)]) -> Result { if deliveries.is_empty() { return Ok(0); } @@ -322,7 +322,7 @@ impl HyperlaneLogStore for HyperlaneSqlDb { .unwrap() .id; StorableDelivery { - message_id: *message_id, + message_id: *message_id.inner(), meta, txn_id, } @@ -338,7 +338,10 @@ impl HyperlaneLogStore for HyperlaneSqlDb { #[async_trait] impl HyperlaneLogStore for HyperlaneSqlDb { - async fn store_logs(&self, payments: &[(InterchainGasPayment, LogMeta)]) -> Result { + async fn store_logs( + &self, + payments: &[(Indexed, LogMeta)], + ) -> Result { if payments.is_empty() { return Ok(0); } @@ -358,7 +361,7 @@ impl HyperlaneLogStore for HyperlaneSqlDb { .unwrap() .id; StorablePayment { - payment, + payment: payment.inner(), meta, txn_id, } diff --git a/rust/chains/hyperlane-cosmos/src/interchain_gas.rs b/rust/chains/hyperlane-cosmos/src/interchain_gas.rs index d9cb4630ef..4ba2ca87ab 100644 --- a/rust/chains/hyperlane-cosmos/src/interchain_gas.rs +++ b/rust/chains/hyperlane-cosmos/src/interchain_gas.rs @@ -3,8 +3,8 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use futures::future; use hyperlane_core::{ ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, - HyperlaneDomain, HyperlaneProvider, Indexer, InterchainGasPaymaster, InterchainGasPayment, - LogMeta, SequenceAwareIndexer, H256, U256, + HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, InterchainGasPaymaster, + InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, U256, }; use once_cell::sync::Lazy; use std::ops::RangeInclusive; @@ -205,7 +205,7 @@ impl Indexer for CosmosInterchainGasPaymasterIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { let logs_futures: Vec<_> = range .map(|block_number| { let self_clone = self.clone(); @@ -239,6 +239,7 @@ impl Indexer for CosmosInterchainGasPaymasterIndexer { .collect::, _>>()? .into_iter() .flatten() + .map(|(log, meta)| (Indexed::new(log), meta)) .collect(); Ok(result) diff --git a/rust/chains/hyperlane-cosmos/src/mailbox.rs b/rust/chains/hyperlane-cosmos/src/mailbox.rs index fcda4e78af..7f686cb85c 100644 --- a/rust/chains/hyperlane-cosmos/src/mailbox.rs +++ b/rust/chains/hyperlane-cosmos/src/mailbox.rs @@ -25,8 +25,8 @@ use tendermint::abci::EventAttribute; use crate::utils::{CONTRACT_ADDRESS_ATTRIBUTE_KEY, CONTRACT_ADDRESS_ATTRIBUTE_KEY_BASE64}; use hyperlane_core::{ utils::bytes_to_hex, ChainResult, HyperlaneChain, HyperlaneContract, HyperlaneDomain, - HyperlaneMessage, HyperlaneProvider, Indexer, LogMeta, Mailbox, TxCostEstimate, TxOutcome, - H256, U256, + HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, LogMeta, Mailbox, TxCostEstimate, + TxOutcome, H256, U256, }; use hyperlane_core::{ ChainCommunicationError, ContractLocator, Decode, RawHyperlaneMessage, SequenceAwareIndexer, @@ -93,7 +93,6 @@ impl HyperlaneChain for CosmosMailbox { impl Debug for CosmosMailbox { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - // Debug::fmt(&(self as &dyn HyperlaneContract), f) todo!() } } @@ -354,7 +353,7 @@ impl Indexer for CosmosMailboxIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { let logs_futures: Vec<_> = range .map(|block_number| { let self_clone = self.clone(); @@ -385,6 +384,7 @@ impl Indexer for CosmosMailboxIndexer { } }) .flatten() + .map(|(log, meta)| (log.into(), meta)) .collect(); Ok(result) @@ -397,7 +397,10 @@ impl Indexer for CosmosMailboxIndexer { #[async_trait] impl Indexer for CosmosMailboxIndexer { - async fn fetch_logs(&self, range: RangeInclusive) -> ChainResult> { + async fn fetch_logs( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { // TODO: implement when implementing Cosmos scraping todo!() } diff --git a/rust/chains/hyperlane-cosmos/src/merkle_tree_hook.rs b/rust/chains/hyperlane-cosmos/src/merkle_tree_hook.rs index 9cab0e9f90..c8e798096c 100644 --- a/rust/chains/hyperlane-cosmos/src/merkle_tree_hook.rs +++ b/rust/chains/hyperlane-cosmos/src/merkle_tree_hook.rs @@ -6,7 +6,7 @@ use futures::future; use hyperlane_core::{ accumulator::incremental::IncrementalMerkle, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, - Indexer, LogMeta, MerkleTreeHook, MerkleTreeInsertion, SequenceAwareIndexer, H256, + Indexed, Indexer, LogMeta, MerkleTreeHook, MerkleTreeInsertion, SequenceAwareIndexer, H256, }; use once_cell::sync::Lazy; use tendermint::abci::EventAttribute; @@ -286,7 +286,7 @@ impl Indexer for CosmosMerkleTreeHookIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { let logs_futures: Vec<_> = range .map(|block_number| { let self_clone = self.clone(); @@ -317,6 +317,7 @@ impl Indexer for CosmosMerkleTreeHookIndexer { } }) .flatten() + .map(|(log, meta)| (log.into(), meta)) .collect(); Ok(result) diff --git a/rust/chains/hyperlane-cosmos/src/trait_builder.rs b/rust/chains/hyperlane-cosmos/src/trait_builder.rs index 23078f3e5e..a6463654f5 100644 --- a/rust/chains/hyperlane-cosmos/src/trait_builder.rs +++ b/rust/chains/hyperlane-cosmos/src/trait_builder.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use derive_new::new; -use hyperlane_core::{ChainCommunicationError, FixedPointNumber}; +use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError, FixedPointNumber}; use url::Url; /// Cosmos connection configuration @@ -25,6 +25,8 @@ pub struct ConnectionConf { /// Cosmos address lengths are sometimes less than 32 bytes, so this helps to serialize it in /// bech32 with the appropriate length. contract_address_bytes: usize, + /// Operation batching configuration + pub operation_batch: OperationBatchConfig, } /// Untyped cosmos amount @@ -112,6 +114,7 @@ impl ConnectionConf { } /// Create a new connection configuration + #[allow(clippy::too_many_arguments)] pub fn new( grpc_urls: Vec, rpc_url: String, @@ -120,6 +123,7 @@ impl ConnectionConf { canonical_asset: String, minimum_gas_price: RawCosmosAmount, contract_address_bytes: usize, + operation_batch: OperationBatchConfig, ) -> Self { Self { grpc_urls, @@ -129,6 +133,7 @@ impl ConnectionConf { canonical_asset, gas_price: minimum_gas_price, contract_address_bytes, + operation_batch, } } } diff --git a/rust/chains/hyperlane-ethereum/Cargo.toml b/rust/chains/hyperlane-ethereum/Cargo.toml index 82b5ff82b0..9f4a5453b3 100644 --- a/rust/chains/hyperlane-ethereum/Cargo.toml +++ b/rust/chains/hyperlane-ethereum/Cargo.toml @@ -17,6 +17,7 @@ ethers-contract.workspace = true ethers-core.workspace = true ethers-signers.workspace = true ethers.workspace = true +eyre.workspace = true futures-util.workspace = true hex.workspace = true num.workspace = true diff --git a/rust/chains/hyperlane-ethereum/src/config.rs b/rust/chains/hyperlane-ethereum/src/config.rs index 334675864c..472c45ff18 100644 --- a/rust/chains/hyperlane-ethereum/src/config.rs +++ b/rust/chains/hyperlane-ethereum/src/config.rs @@ -1,4 +1,4 @@ -use hyperlane_core::U256; +use hyperlane_core::{config::OperationBatchConfig, U256}; use url::Url; /// Ethereum RPC connection configuration @@ -33,6 +33,8 @@ pub struct ConnectionConf { pub rpc_connection: RpcConnectionConf, /// Transaction overrides to use when sending transactions. pub transaction_overrides: TransactionOverrides, + /// Operation batching configuration + pub operation_batch: OperationBatchConfig, } /// Ethereum transaction overrides. diff --git a/rust/chains/hyperlane-ethereum/src/contracts/interchain_gas.rs b/rust/chains/hyperlane-ethereum/src/contracts/interchain_gas.rs index c51e8d0ef3..8ed514c836 100644 --- a/rust/chains/hyperlane-ethereum/src/contracts/interchain_gas.rs +++ b/rust/chains/hyperlane-ethereum/src/contracts/interchain_gas.rs @@ -9,8 +9,8 @@ use async_trait::async_trait; use ethers::prelude::Middleware; use hyperlane_core::{ ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain, - HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexer, InterchainGasPaymaster, - InterchainGasPayment, LogMeta, SequenceAwareIndexer, H160, H256, + HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, + InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H160, H256, }; use tracing::instrument; @@ -89,7 +89,7 @@ where async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { let events = self .contract .gas_payment_filter() @@ -102,12 +102,12 @@ where .into_iter() .map(|(log, log_meta)| { ( - InterchainGasPayment { + Indexed::new(InterchainGasPayment { message_id: H256::from(log.message_id), destination: log.destination_domain, payment: log.payment.into(), gas_amount: log.gas_amount.into(), - }, + }), log_meta.into(), ) }) diff --git a/rust/chains/hyperlane-ethereum/src/contracts/mailbox.rs b/rust/chains/hyperlane-ethereum/src/contracts/mailbox.rs index 3b030fbd32..fd6a6b2808 100644 --- a/rust/chains/hyperlane-ethereum/src/contracts/mailbox.rs +++ b/rust/chains/hyperlane-ethereum/src/contracts/mailbox.rs @@ -7,18 +7,20 @@ use std::ops::RangeInclusive; use std::sync::Arc; use async_trait::async_trait; -use ethers::abi::AbiEncode; +use ethers::abi::{AbiEncode, Detokenize}; use ethers::prelude::Middleware; use ethers_contract::builders::ContractCall; +use futures_util::future::join_all; use tracing::instrument; use hyperlane_core::{ - utils::bytes_to_hex, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, - HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage, HyperlaneProtocolError, - HyperlaneProvider, Indexer, LogMeta, Mailbox, RawHyperlaneMessage, SequenceAwareIndexer, - TxCostEstimate, TxOutcome, H160, H256, U256, + utils::bytes_to_hex, BatchItem, ChainCommunicationError, ChainResult, ContractLocator, + HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage, + HyperlaneProtocolError, HyperlaneProvider, Indexed, Indexer, LogMeta, Mailbox, + RawHyperlaneMessage, SequenceAwareIndexer, TxCostEstimate, TxOutcome, H160, H256, U256, }; +use crate::error::HyperlaneEthereumError; use crate::interfaces::arbitrum_node_interface::ArbitrumNodeInterface; use crate::interfaces::i_mailbox::{ IMailbox as EthereumMailboxInternal, ProcessCall, IMAILBOX_ABI, @@ -26,6 +28,8 @@ use crate::interfaces::i_mailbox::{ use crate::tx::{call_with_lag, fill_tx_gas_params, report_tx}; use crate::{BuildableWithProvider, ConnectionConf, EthereumProvider, TransactionOverrides}; +use super::multicall::{self, build_multicall}; + impl std::fmt::Display for EthereumMailboxInternal where M: Middleware, @@ -133,8 +137,8 @@ where async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { - let mut events: Vec<(HyperlaneMessage, LogMeta)> = self + ) -> ChainResult, LogMeta)>> { + let mut events: Vec<(Indexed, LogMeta)> = self .contract .dispatch_filter() .from_block(*range.start()) @@ -142,10 +146,15 @@ where .query_with_meta() .await? .into_iter() - .map(|(event, meta)| (HyperlaneMessage::from(event.message.to_vec()), meta.into())) + .map(|(event, meta)| { + ( + HyperlaneMessage::from(event.message.to_vec()).into(), + meta.into(), + ) + }) .collect(); - events.sort_by(|a, b| a.0.nonce.cmp(&b.0.nonce)); + events.sort_by(|a, b| a.0.inner().nonce.cmp(&b.0.inner().nonce)); Ok(events) } } @@ -174,7 +183,10 @@ where /// Note: This call may return duplicates depending on the provider used #[instrument(err, skip(self))] - async fn fetch_logs(&self, range: RangeInclusive) -> ChainResult> { + async fn fetch_logs( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { Ok(self .contract .process_id_filter() @@ -183,7 +195,7 @@ where .query_with_meta() .await? .into_iter() - .map(|(event, meta)| (H256::from(event.message_id), meta.into())) + .map(|(event, meta)| (Indexed::new(H256::from(event.message_id)), meta.into())) .collect()) } } @@ -272,6 +284,14 @@ where metadata.to_vec().into(), RawHyperlaneMessage::from(message).to_vec().into(), ); + self.add_gas_overrides(tx, tx_gas_estimate).await + } + + async fn add_gas_overrides( + &self, + tx: ContractCall, + tx_gas_estimate: Option, + ) -> ChainResult> { let tx_overrides = TransactionOverrides { // If a gas limit is provided as a transaction override, use it instead // of the estimate. @@ -357,6 +377,37 @@ where Ok(receipt.into()) } + #[instrument(skip(self, messages), fields(size=%messages.len()))] + async fn process_batch( + &self, + messages: &[BatchItem], + ) -> ChainResult { + let mut multicall = build_multicall(self.provider.clone(), &self.conn, self.domain.clone()) + .await + .map_err(|e| HyperlaneEthereumError::MulticallError(e.to_string()))?; + let contract_call_futures = messages + .iter() + .map(|batch_item| async { + self.process_contract_call( + &batch_item.data, + &batch_item.submission_data.metadata, + Some(batch_item.submission_data.gas_limit), + ) + .await + }) + .collect::>(); + let contract_calls = join_all(contract_call_futures) + .await + .into_iter() + .collect::>>()?; + + let batch_call = multicall::batch::<_, ()>(&mut multicall, contract_calls); + let call = self.add_gas_overrides(batch_call, None).await?; + + let receipt = report_tx(call).await?; + Ok(receipt.into()) + } + #[instrument(skip(self), fields(msg=%message, metadata=%bytes_to_hex(metadata)))] async fn process_estimate_costs( &self, @@ -442,7 +493,7 @@ mod test { use crate::{contracts::EthereumMailbox, ConnectionConf, RpcConnectionConf}; /// An amount of gas to add to the estimated gas - const GAS_ESTIMATE_BUFFER: u32 = 50000; + const GAS_ESTIMATE_BUFFER: u32 = 75_000; #[tokio::test] async fn test_process_estimate_costs_sets_l2_gas_limit_for_arbitrum() { @@ -453,6 +504,7 @@ mod test { url: "http://127.0.0.1:8545".parse().unwrap(), }, transaction_overrides: Default::default(), + operation_batch: Default::default(), }; let mailbox = EthereumMailbox::new( diff --git a/rust/chains/hyperlane-ethereum/src/contracts/merkle_tree_hook.rs b/rust/chains/hyperlane-ethereum/src/contracts/merkle_tree_hook.rs index f920c28ccc..a94ceff325 100644 --- a/rust/chains/hyperlane-ethereum/src/contracts/merkle_tree_hook.rs +++ b/rust/chains/hyperlane-ethereum/src/contracts/merkle_tree_hook.rs @@ -10,8 +10,8 @@ use tracing::instrument; use hyperlane_core::{ ChainCommunicationError, ChainResult, Checkpoint, ContractLocator, HyperlaneChain, - HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexer, LogMeta, MerkleTreeHook, - MerkleTreeInsertion, SequenceAwareIndexer, H256, + HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, LogMeta, + MerkleTreeHook, MerkleTreeInsertion, SequenceAwareIndexer, H256, }; use crate::interfaces::merkle_tree_hook::{MerkleTreeHook as MerkleTreeHookContract, Tree}; @@ -111,7 +111,7 @@ where async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { let events = self .contract .inserted_into_tree_filter() @@ -124,7 +124,7 @@ where .into_iter() .map(|(log, log_meta)| { ( - MerkleTreeInsertion::new(log.index, H256::from(log.message_id)), + MerkleTreeInsertion::new(log.index, H256::from(log.message_id)).into(), log_meta.into(), ) }) diff --git a/rust/chains/hyperlane-ethereum/src/contracts/mod.rs b/rust/chains/hyperlane-ethereum/src/contracts/mod.rs index 2b85733f69..32ad5b953d 100644 --- a/rust/chains/hyperlane-ethereum/src/contracts/mod.rs +++ b/rust/chains/hyperlane-ethereum/src/contracts/mod.rs @@ -6,4 +6,6 @@ mod mailbox; mod merkle_tree_hook; +mod multicall; + mod validator_announce; diff --git a/rust/chains/hyperlane-ethereum/src/contracts/multicall.rs b/rust/chains/hyperlane-ethereum/src/contracts/multicall.rs new file mode 100644 index 0000000000..291479aeb0 --- /dev/null +++ b/rust/chains/hyperlane-ethereum/src/contracts/multicall.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use ethers::{abi::Detokenize, providers::Middleware}; +use ethers_contract::{builders::ContractCall, Multicall, MulticallResult, MulticallVersion}; +use hyperlane_core::{utils::hex_or_base58_to_h256, HyperlaneDomain, HyperlaneProvider}; + +use crate::{ConnectionConf, EthereumProvider}; + +const ALLOW_BATCH_FAILURES: bool = true; + +pub async fn build_multicall( + provider: Arc, + conn: &ConnectionConf, + domain: HyperlaneDomain, +) -> eyre::Result> { + let address = conn + .operation_batch + .batch_contract_address + .unwrap_or(hex_or_base58_to_h256("0xcA11bde05977b3631167028862bE2a173976CA11").unwrap()); + let ethereum_provider = EthereumProvider::new(provider.clone(), domain); + if !ethereum_provider.is_contract(&address).await? { + return Err(eyre::eyre!("Multicall contract not found at address")); + } + let multicall = match Multicall::new(provider.clone(), Some(address.into())).await { + Ok(multicall) => multicall.version(MulticallVersion::Multicall3), + Err(err) => { + return Err(eyre::eyre!( + "Unable to build multicall contract: {}", + err.to_string() + )) + } + }; + + Ok(multicall) +} + +pub fn batch( + multicall: &mut Multicall, + calls: Vec>, +) -> ContractCall> { + // clear any calls that were in the multicall beforehand + multicall.clear_calls(); + + calls.into_iter().for_each(|call| { + multicall.add_call(call, ALLOW_BATCH_FAILURES); + }); + + multicall.as_aggregate_3_value() +} diff --git a/rust/chains/hyperlane-ethereum/src/error.rs b/rust/chains/hyperlane-ethereum/src/error.rs index 597703cf85..6b7183187b 100644 --- a/rust/chains/hyperlane-ethereum/src/error.rs +++ b/rust/chains/hyperlane-ethereum/src/error.rs @@ -11,6 +11,10 @@ pub enum HyperlaneEthereumError { #[error("{0}")] ProviderError(#[from] ProviderError), + /// multicall Error + #[error("Multicall contract error: {0}")] + MulticallError(String), + /// Some details from a queried block are missing #[error("Some details from a queried block are missing")] MissingBlockDetails, diff --git a/rust/chains/hyperlane-ethereum/src/tx.rs b/rust/chains/hyperlane-ethereum/src/tx.rs index a8938dcf15..5ec0ebd386 100644 --- a/rust/chains/hyperlane-ethereum/src/tx.rs +++ b/rust/chains/hyperlane-ethereum/src/tx.rs @@ -5,7 +5,7 @@ use std::time::Duration; use ethers::{ abi::Detokenize, prelude::{NameOrAddress, TransactionReceipt}, - providers::ProviderError, + providers::{JsonRpcClient, PendingTransaction, ProviderError}, types::Eip1559TransactionRequest, }; use ethers_contract::builders::ContractCall; @@ -22,7 +22,7 @@ use tracing::{error, info}; use crate::{Middleware, TransactionOverrides}; /// An amount of gas to add to the estimated gas -const GAS_ESTIMATE_BUFFER: u32 = 50000; +pub const GAS_ESTIMATE_BUFFER: u32 = 75_000; const PENDING_TRANSACTION_POLLING_INTERVAL: Duration = Duration::from_secs(2); @@ -50,12 +50,17 @@ where let dispatched = dispatch_fut .await? .interval(PENDING_TRANSACTION_POLLING_INTERVAL); + track_pending_tx(dispatched).await +} - let tx_hash: H256 = (*dispatched).into(); +pub(crate) async fn track_pending_tx( + pending_tx: PendingTransaction<'_, P>, +) -> ChainResult { + let tx_hash: H256 = (*pending_tx).into(); - info!(?to, %data, ?tx_hash, "Dispatched tx"); + info!(?tx_hash, "Dispatched tx"); - match tokio::time::timeout(Duration::from_secs(150), dispatched).await { + match tokio::time::timeout(Duration::from_secs(150), pending_tx).await { // all good Ok(Ok(Some(receipt))) => { info!(?tx_hash, "confirmed transaction"); @@ -87,7 +92,7 @@ where M: Middleware + 'static, D: Detokenize, { - let gas_limit = if let Some(gas_limit) = transaction_overrides.gas_limit { + let gas_limit: U256 = if let Some(gas_limit) = transaction_overrides.gas_limit { gas_limit } else { tx.estimate_gas() diff --git a/rust/chains/hyperlane-fuel/src/interchain_gas.rs b/rust/chains/hyperlane-fuel/src/interchain_gas.rs index 85fb630a34..d969210a60 100644 --- a/rust/chains/hyperlane-fuel/src/interchain_gas.rs +++ b/rust/chains/hyperlane-fuel/src/interchain_gas.rs @@ -3,7 +3,7 @@ use std::ops::RangeInclusive; use async_trait::async_trait; use hyperlane_core::{ - ChainResult, HyperlaneChain, HyperlaneContract, Indexer, InterchainGasPaymaster, + ChainResult, HyperlaneChain, HyperlaneContract, Indexed, Indexer, InterchainGasPaymaster, }; use hyperlane_core::{HyperlaneDomain, HyperlaneProvider, InterchainGasPayment, LogMeta, H256}; @@ -38,7 +38,7 @@ impl Indexer for FuelInterchainGasPaymasterIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { todo!() } diff --git a/rust/chains/hyperlane-fuel/src/mailbox.rs b/rust/chains/hyperlane-fuel/src/mailbox.rs index dbf130235f..035fe6e6d3 100644 --- a/rust/chains/hyperlane-fuel/src/mailbox.rs +++ b/rust/chains/hyperlane-fuel/src/mailbox.rs @@ -5,6 +5,7 @@ use std::ops::RangeInclusive; use async_trait::async_trait; use fuels::prelude::{Bech32ContractId, WalletUnlocked}; +use hyperlane_core::Indexed; use tracing::instrument; use hyperlane_core::{ @@ -128,7 +129,7 @@ impl Indexer for FuelMailboxIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { todo!() } @@ -139,7 +140,10 @@ impl Indexer for FuelMailboxIndexer { #[async_trait] impl Indexer for FuelMailboxIndexer { - async fn fetch_logs(&self, range: RangeInclusive) -> ChainResult> { + async fn fetch_logs( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { todo!() } diff --git a/rust/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs index cb4a3543e9..4945833818 100644 --- a/rust/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use hyperlane_core::{ config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator, - HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexer, + HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, H512, }; use hyperlane_sealevel_igp::{ @@ -106,7 +106,7 @@ pub struct SealevelInterchainGasPaymasterIndexer { /// IGP payment data on Sealevel #[derive(Debug, new)] pub struct SealevelGasPayment { - payment: InterchainGasPayment, + payment: Indexed, log_meta: LogMeta, igp_account_pubkey: H256, } @@ -223,7 +223,11 @@ impl SealevelInterchainGasPaymasterIndexer { }; Ok(SealevelGasPayment::new( - igp_payment, + Indexed::new(igp_payment).with_sequence( + sequence_number + .try_into() + .map_err(StrOrIntParseError::from)?, + ), LogMeta { address: self.igp.program_id.to_bytes().into(), block_number: gas_payment_account.slot, @@ -245,7 +249,7 @@ impl Indexer for SealevelInterchainGasPaymasterIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { info!( ?range, "Fetching SealevelInterchainGasPaymasterIndexer InterchainGasPayment logs" diff --git a/rust/chains/hyperlane-sealevel/src/mailbox.rs b/rust/chains/hyperlane-sealevel/src/mailbox.rs index bcf79be867..3fc8393d14 100644 --- a/rust/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/chains/hyperlane-sealevel/src/mailbox.rs @@ -8,10 +8,11 @@ use jsonrpc_core::futures_util::TryFutureExt; use tracing::{debug, info, instrument, warn}; use hyperlane_core::{ - accumulator::incremental::IncrementalMerkle, ChainCommunicationError, ChainResult, Checkpoint, - ContractLocator, Decode as _, Encode as _, FixedPointNumber, HyperlaneAbi, HyperlaneChain, - HyperlaneContract, HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexer, LogMeta, - Mailbox, MerkleTreeHook, SequenceAwareIndexer, TxCostEstimate, TxOutcome, H256, H512, U256, + accumulator::incremental::IncrementalMerkle, BatchItem, ChainCommunicationError, ChainResult, + Checkpoint, ContractLocator, Decode as _, Encode as _, FixedPointNumber, HyperlaneAbi, + HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, + Indexed, Indexer, LogMeta, Mailbox, MerkleTreeHook, SequenceAwareIndexer, TxCostEstimate, + TxOutcome, H256, H512, U256, }; use hyperlane_sealevel_interchain_security_module_interface::{ InterchainSecurityModuleInstruction, VerifyInstruction, @@ -527,7 +528,10 @@ impl SealevelMailboxIndexer { Ok(height) } - async fn get_message_with_nonce(&self, nonce: u32) -> ChainResult<(HyperlaneMessage, LogMeta)> { + async fn get_message_with_nonce( + &self, + nonce: u32, + ) -> ChainResult<(Indexed, LogMeta)> { let target_message_account_bytes = &[ &hyperlane_sealevel_mailbox::accounts::DISPATCHED_MESSAGE_DISCRIMINATOR[..], &nonce.to_le_bytes()[..], @@ -614,7 +618,7 @@ impl SealevelMailboxIndexer { HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?; Ok(( - hyperlane_message, + hyperlane_message.into(), LogMeta { address: self.mailbox.program_id.to_bytes().into(), block_number: dispatched_message_account.slot, @@ -645,7 +649,7 @@ impl Indexer for SealevelMailboxIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { info!( ?range, "Fetching SealevelMailboxIndexer HyperlaneMessage logs" @@ -666,7 +670,10 @@ impl Indexer for SealevelMailboxIndexer { #[async_trait] impl Indexer for SealevelMailboxIndexer { - async fn fetch_logs(&self, _range: RangeInclusive) -> ChainResult> { + async fn fetch_logs( + &self, + _range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { todo!() } diff --git a/rust/chains/hyperlane-sealevel/src/merkle_tree_hook.rs b/rust/chains/hyperlane-sealevel/src/merkle_tree_hook.rs index 30dea8c535..9fe48053c8 100644 --- a/rust/chains/hyperlane-sealevel/src/merkle_tree_hook.rs +++ b/rust/chains/hyperlane-sealevel/src/merkle_tree_hook.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use derive_new::new; use hyperlane_core::{ accumulator::incremental::IncrementalMerkle, ChainCommunicationError, ChainResult, Checkpoint, - HyperlaneChain, HyperlaneMessage, Indexer, LogMeta, MerkleTreeHook, MerkleTreeInsertion, - SequenceAwareIndexer, + HyperlaneChain, HyperlaneMessage, Indexed, Indexer, LogMeta, MerkleTreeHook, + MerkleTreeInsertion, SequenceAwareIndexer, }; use hyperlane_sealevel_mailbox::accounts::OutboxAccount; use solana_sdk::commitment_config::CommitmentConfig; @@ -86,11 +86,11 @@ impl Indexer for SealevelMerkleTreeHookIndexer { async fn fetch_logs( &self, range: RangeInclusive, - ) -> ChainResult> { + ) -> ChainResult, LogMeta)>> { let messages = Indexer::::fetch_logs(&self.0, range).await?; let merkle_tree_insertions = messages .into_iter() - .map(|(m, meta)| (message_to_merkle_tree_insertion(&m), meta)) + .map(|(m, meta)| (message_to_merkle_tree_insertion(m.inner()).into(), meta)) .collect(); Ok(merkle_tree_insertions) } diff --git a/rust/chains/hyperlane-sealevel/src/trait_builder.rs b/rust/chains/hyperlane-sealevel/src/trait_builder.rs index 8b14b868e0..416b888df9 100644 --- a/rust/chains/hyperlane-sealevel/src/trait_builder.rs +++ b/rust/chains/hyperlane-sealevel/src/trait_builder.rs @@ -1,4 +1,4 @@ -use hyperlane_core::ChainCommunicationError; +use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError}; use url::Url; /// Sealevel connection configuration @@ -6,6 +6,8 @@ use url::Url; pub struct ConnectionConf { /// Fully qualified string to connect to pub url: Url, + /// Operation batching configuration + pub operation_batch: OperationBatchConfig, } /// An error type when parsing a connection configuration. diff --git a/rust/config/mainnet_config.json b/rust/config/mainnet_config.json index 303daf485c..65648774fc 100644 --- a/rust/config/mainnet_config.json +++ b/rust/config/mainnet_config.json @@ -1,6 +1,7 @@ { "chains": { "ancient8": { + "batchContractAddress": "0x4C97D35c668EE5194a13c8DE8Afc18cce40C9F28", "blockExplorers": [ { "apiUrl": "https://scan.ancient8.gg/api", diff --git a/rust/hyperlane-base/src/contract_sync/cursors/mod.rs b/rust/hyperlane-base/src/contract_sync/cursors/mod.rs index 546616f4f3..c7d7274d68 100644 --- a/rust/hyperlane-base/src/contract_sync/cursors/mod.rs +++ b/rust/hyperlane-base/src/contract_sync/cursors/mod.rs @@ -33,7 +33,7 @@ impl Indexable for InterchainGasPayment { match domain { HyperlaneDomainProtocol::Ethereum => CursorType::RateLimited, HyperlaneDomainProtocol::Fuel => todo!(), - HyperlaneDomainProtocol::Sealevel => CursorType::RateLimited, + HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::RateLimited, } } @@ -55,7 +55,7 @@ impl Indexable for Delivery { match domain { HyperlaneDomainProtocol::Ethereum => CursorType::RateLimited, HyperlaneDomainProtocol::Fuel => todo!(), - HyperlaneDomainProtocol::Sealevel => CursorType::RateLimited, + HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::RateLimited, } } diff --git a/rust/hyperlane-base/src/contract_sync/cursors/rate_limited.rs b/rust/hyperlane-base/src/contract_sync/cursors/rate_limited.rs index b3476db646..d85b3618f6 100644 --- a/rust/hyperlane-base/src/contract_sync/cursors/rate_limited.rs +++ b/rust/hyperlane-base/src/contract_sync/cursors/rate_limited.rs @@ -9,19 +9,14 @@ use async_trait::async_trait; use derive_new::new; use eyre::Result; use hyperlane_core::{ - ChainCommunicationError, ContractSyncCursor, CursorAction, HyperlaneWatermarkedLogStore, - IndexMode, Indexer, LogMeta, SequenceAwareIndexer, + ContractSyncCursor, CursorAction, HyperlaneWatermarkedLogStore, Indexed, Indexer, LogMeta, }; -use tokio::time::sleep; -use tracing::warn; use crate::contract_sync::eta_calculator::SyncerEtaCalculator; /// Time window for the moving average used in the eta calculator in seconds. const ETA_TIME_WINDOW: f64 = 2. * 60.; -const MAX_SEQUENCE_RANGE: u32 = 20; - #[derive(Debug, new)] pub(crate) struct SyncState { chunk_size: u32, @@ -29,39 +24,13 @@ pub(crate) struct SyncState { start_block: u32, /// The next block that should be indexed. next_block: u32, - mode: IndexMode, - /// The next sequence index that the cursor is looking for. - /// In the EVM, this is used for optimizing indexing, - /// because it's cheaper to make read calls for the sequence index than - /// to call `eth_getLogs` with a block range. - /// In Sealevel, historic queries aren't supported, so the sequence field - /// is used to query storage in sequence. - next_sequence: u32, direction: SyncDirection, } impl SyncState { - async fn get_next_range( - &mut self, - max_sequence: Option, - tip: u32, - ) -> Result>> { + async fn get_next_range(&self, tip: u32) -> Result>> { // We attempt to index a range of blocks that is as large as possible. - let range = match self.mode { - IndexMode::Block => self.block_range(tip), - IndexMode::Sequence => { - let max_sequence = max_sequence.ok_or_else(|| { - ChainCommunicationError::from_other_str( - "Sequence indexing requires a max sequence", - ) - })?; - if let Some(range) = self.sequence_range(max_sequence)? { - range - } else { - return Ok(None); - } - } - }; + let range = self.block_range(tip); if range.is_empty() { return Ok(None); } @@ -85,52 +54,14 @@ impl SyncState { from..=to } - /// Returns the next sequence range to index. - /// - /// # Arguments - /// - /// * `tip` - The current tip of the chain. - /// * `max_sequence` - The maximum sequence that should be indexed. - /// `max_sequence` is the exclusive upper bound of the range to be indexed. - /// (e.g. `0..max_sequence`) - fn sequence_range(&self, max_sequence: u32) -> Result>> { - let (from, to) = match self.direction { + fn update_range(&mut self, range: RangeInclusive) { + match self.direction { SyncDirection::Forward => { - let sequence_start = self.next_sequence; - let mut sequence_end = sequence_start + MAX_SEQUENCE_RANGE; - if self.next_sequence >= max_sequence { - return Ok(None); - } - sequence_end = u32::min(sequence_end, max_sequence.saturating_sub(1)); - (sequence_start, sequence_end) + self.next_block = *range.end() + 1; } SyncDirection::Backward => { - let sequence_end = self.next_sequence; - let sequence_start = sequence_end.saturating_sub(MAX_SEQUENCE_RANGE); - (sequence_start, sequence_end) + self.next_block = range.start().saturating_sub(1); } - }; - Ok(Some(from..=to)) - } - - fn update_range(&mut self, range: RangeInclusive) { - match self.direction { - SyncDirection::Forward => match self.mode { - IndexMode::Block => { - self.next_block = *range.end() + 1; - } - IndexMode::Sequence => { - self.next_sequence = *range.end() + 1; - } - }, - SyncDirection::Backward => match self.mode { - IndexMode::Block => { - self.next_block = range.start().saturating_sub(1); - } - IndexMode::Sequence => { - self.next_sequence = range.start().saturating_sub(1); - } - }, } } } @@ -146,10 +77,9 @@ pub enum SyncDirection { /// queried is and also handling rate limiting. Rate limiting is automatically /// performed by `next_action`. pub(crate) struct RateLimitedContractSyncCursor { - indexer: Arc>, + indexer: Arc>, db: Arc>, tip: u32, - max_sequence: Option, last_tip_update: Instant, eta_calculator: SyncerEtaCalculator, sync_state: SyncState, @@ -158,26 +88,22 @@ pub(crate) struct RateLimitedContractSyncCursor { impl RateLimitedContractSyncCursor { /// Construct a new contract sync helper. pub async fn new( - indexer: Arc>, + indexer: Arc>, db: Arc>, chunk_size: u32, initial_height: u32, - mode: IndexMode, ) -> Result { - let (max_sequence, tip) = indexer.latest_sequence_count_and_tip().await?; + let tip = indexer.get_finalized_block_number().await?; Ok(Self { indexer, db, tip, - max_sequence, last_tip_update: Instant::now(), eta_calculator: SyncerEtaCalculator::new(initial_height, tip, ETA_TIME_WINDOW), sync_state: SyncState::new( chunk_size, initial_height, initial_height, - mode, - Default::default(), // The rate limited cursor currently only syncs in the forward direction. SyncDirection::Forward, ), @@ -186,86 +112,58 @@ impl RateLimitedContractSyncCursor { /// Wait based on how close we are to the tip and update the tip, /// i.e. the highest block we may scrape. - async fn get_rate_limit(&mut self) -> Result> { + async fn get_rate_limit(&self) -> Result> { if self.sync_state.next_block + self.sync_state.chunk_size < self.tip { // If doing the full chunk wouldn't exceed the already known tip we do not need to rate limit. - Ok(None) - } else { - // We are within one chunk size of the known tip. - // If it's been fewer than 30s since the last tip update, sleep for a bit until we're ready to fetch the next tip. - if let Some(sleep_time) = - Duration::from_secs(30).checked_sub(self.last_tip_update.elapsed()) - { - return Ok(Some(sleep_time)); - } - match self.indexer.get_finalized_block_number().await { - Ok(tip) => { - // we retrieved a new tip value, go ahead and update. - self.last_tip_update = Instant::now(); - self.tip = tip; - Ok(None) - } - Err(e) => { - warn!(error = %e, "Failed to get next block range because we could not get the current tip"); - // we are failing to make a basic query, we should wait before retrying. - sleep(Duration::from_secs(10)).await; - Err(e.into()) - } - } + return Ok(None); } - } - fn sync_end(&self) -> Result { - match self.sync_state.mode { - IndexMode::Block => Ok(self.tip), - IndexMode::Sequence => self - .max_sequence - .ok_or(eyre::eyre!("Sequence indexing requires a max sequence",)), + // We are within one chunk size of the known tip. + // If it's been fewer than 30s since the last tip update, sleep for a bit until we're ready to fetch the next tip. + if let Some(sleep_time) = + Duration::from_secs(30).checked_sub(self.last_tip_update.elapsed()) + { + return Ok(Some(sleep_time)); } + Ok(None) + } + + fn sync_end(&self) -> u32 { + self.tip } fn sync_position(&self) -> u32 { - match self.sync_state.mode { - IndexMode::Block => self.sync_state.next_block, - IndexMode::Sequence => self.sync_state.next_sequence, - } + self.sync_state.next_block } fn sync_step(&self) -> u32 { - match self.sync_state.mode { - IndexMode::Block => self.sync_state.chunk_size, - IndexMode::Sequence => MAX_SEQUENCE_RANGE, - } + self.sync_state.chunk_size } - async fn get_next_range(&mut self) -> Result>> { - let (max_sequence, tip) = self.indexer.latest_sequence_count_and_tip().await?; - self.tip = tip; - self.max_sequence = max_sequence; - - self.sync_state.get_next_range(max_sequence, tip).await + async fn get_next_range(&self) -> Result>> { + let tip = self.indexer.get_finalized_block_number().await?; + self.sync_state.get_next_range(tip).await } - fn sync_eta(&mut self) -> Result { - let sync_end = self.sync_end()?; + fn sync_eta(&mut self) -> Duration { + let sync_end = self.sync_end(); let to = u32::min(sync_end, self.sync_position() + self.sync_step()); let from = self.sync_position(); - let eta = if to < sync_end { + if to < sync_end { self.eta_calculator.calculate(from, sync_end) } else { Duration::from_secs(0) - }; - Ok(eta) + } } } #[async_trait] impl ContractSyncCursor for RateLimitedContractSyncCursor where - T: Send + Debug + 'static, + T: Send + Sync + Debug + 'static, { async fn next_action(&mut self) -> Result<(CursorAction, Duration)> { - let eta = self.sync_eta()?; + let eta = self.sync_eta(); let rate_limit = self.get_rate_limit().await?; if let Some(rate_limit) = rate_limit { @@ -284,7 +182,11 @@ where self.sync_state.next_block.saturating_sub(1) } - async fn update(&mut self, _: Vec<(T, LogMeta)>, range: RangeInclusive) -> Result<()> { + async fn update( + &mut self, + _: Vec<(Indexed, LogMeta)>, + range: RangeInclusive, + ) -> Result<()> { // Store a relatively conservative view of the high watermark, which should allow a single watermark to be // safely shared across multiple cursors, so long as they are running sufficiently in sync self.db @@ -296,7 +198,21 @@ where )) .await?; self.sync_state.update_range(range); - Ok(()) + + match self.indexer.get_finalized_block_number().await { + Ok(tip) => { + // we retrieved a new tip value, go ahead and update. + self.last_tip_update = Instant::now(); + self.tip = tip; + Ok(()) + } + Err(e) => { + return Err(eyre::eyre!( + "Failed to update the cursor because we could not get the current tip: {}", + e + )) + } + } } } @@ -318,14 +234,9 @@ pub(crate) mod test { #[async_trait] impl Indexer<()> for Indexer { - async fn fetch_logs(&self, range: RangeInclusive) -> ChainResult>; + async fn fetch_logs(&self, range: RangeInclusive) -> ChainResult , LogMeta)>>; async fn get_finalized_block_number(&self) -> ChainResult; } - - #[async_trait] - impl SequenceAwareIndexer<()> for Indexer { - async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)>; - } } mockall::mock! { @@ -337,7 +248,7 @@ pub(crate) mod test { #[async_trait] impl HyperlaneLogStore<()> for Db { - async fn store_logs(&self, logs: &[((), LogMeta)]) -> Result; + async fn store_logs(&self, logs: &[(hyperlane_core::Indexed<()> , LogMeta)]) -> Result; } #[async_trait] @@ -356,16 +267,19 @@ pub(crate) mod test { Some(chain_tips) => { for tip in chain_tips { indexer - .expect_latest_sequence_count_and_tip() + .expect_get_finalized_block_number() .times(1) .in_sequence(&mut seq) - .returning(move || Ok((None, tip))); + .returning(move || Ok(tip)); } } None => { indexer - .expect_latest_sequence_count_and_tip() - .returning(move || Ok((None, 100))); + .expect_get_finalized_block_number() + .returning(move || Ok(100)); + indexer + .expect_get_finalized_block_number() + .returning(move || Ok(100)); } } @@ -373,13 +287,11 @@ pub(crate) mod test { db.expect_store_high_watermark().returning(|_| Ok(())); let chunk_size = CHUNK_SIZE; let initial_height = INITIAL_HEIGHT; - let mode = IndexMode::Block; RateLimitedContractSyncCursor::new( Arc::new(indexer), Arc::new(db), chunk_size, initial_height, - mode, ) .await .unwrap() @@ -389,10 +301,10 @@ pub(crate) mod test { async fn test_next_action_retries_if_update_isnt_called() { let mut cursor = mock_rate_limited_cursor(None).await; let (action_1, _) = cursor.next_action().await.unwrap(); - let (action_2, _) = cursor.next_action().await.unwrap(); + let (_action_2, _) = cursor.next_action().await.unwrap(); // Calling next_action without updating the cursor should return the same action - assert!(matches!(action_1, action_2)); + assert!(matches!(action_1, _action_2)); } #[tokio::test] @@ -407,8 +319,8 @@ pub(crate) mod test { cursor.update(vec![], range.clone()).await.unwrap(); let (action_3, _) = cursor.next_action().await.unwrap(); - let expected_range = range.end() + 1..=(range.end() + CHUNK_SIZE); - assert!(matches!(action_3, CursorAction::Query(expected_range))); + let _expected_range = range.end() + 1..=(range.end() + CHUNK_SIZE); + assert!(matches!(action_3, CursorAction::Query(_expected_range))); } #[tokio::test] diff --git a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs index 790c9f3f0f..e217d4bb42 100644 --- a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs +++ b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs @@ -5,8 +5,8 @@ use std::{collections::HashSet, fmt::Debug, ops::RangeInclusive, sync::Arc, time use async_trait::async_trait; use eyre::Result; use hyperlane_core::{ - ContractSyncCursor, CursorAction, HyperlaneSequenceAwareIndexerStoreReader, IndexMode, LogMeta, - Sequenced, + indexed_to_sequence_indexed_array, ContractSyncCursor, CursorAction, + HyperlaneSequenceAwareIndexerStoreReader, IndexMode, Indexed, LogMeta, SequenceIndexed, }; use itertools::Itertools; use tracing::{debug, warn}; @@ -33,7 +33,7 @@ pub(crate) struct BackwardSequenceAwareSyncCursor { index_mode: IndexMode, } -impl BackwardSequenceAwareSyncCursor { +impl BackwardSequenceAwareSyncCursor { pub fn new( chunk_size: u32, db: Arc>, @@ -161,7 +161,7 @@ impl BackwardSequenceAwareSyncCursor { /// - If there are any gaps, the cursor rewinds to the last indexed snapshot, and ranges will be retried. fn update_block_range( &mut self, - logs: Vec<(T, LogMeta)>, + logs: Vec<(SequenceIndexed, LogMeta)>, all_log_sequences: &HashSet, range: RangeInclusive, current_indexing_snapshot: TargetSnapshot, @@ -198,7 +198,7 @@ impl BackwardSequenceAwareSyncCursor { if let Some(lowest_sequence_log) = logs.first() { // Update the last snapshot. self.last_indexed_snapshot = LastIndexedSnapshot { - sequence: Some(lowest_sequence_log.0.sequence()), + sequence: Some(lowest_sequence_log.0.sequence), at_block: lowest_sequence_log.1.block_number.try_into()?, }; } @@ -215,7 +215,7 @@ impl BackwardSequenceAwareSyncCursor { /// - If there are any gaps, the cursor rewinds and the range will be retried. fn update_sequence_range( &mut self, - logs: Vec<(T, LogMeta)>, + logs: Vec<(SequenceIndexed, LogMeta)>, all_log_sequences: &HashSet, range: RangeInclusive, current_indexing_snapshot: TargetSnapshot, @@ -261,7 +261,7 @@ impl BackwardSequenceAwareSyncCursor { // Update the last indexed snapshot. self.last_indexed_snapshot = LastIndexedSnapshot { - sequence: Some(lowest_sequence_log.0.sequence()), + sequence: Some(lowest_sequence_log.0.sequence), at_block: lowest_sequence_log.1.block_number.try_into()?, }; // Position the current snapshot to the previous sequence. @@ -274,7 +274,7 @@ impl BackwardSequenceAwareSyncCursor { /// and logs the inconsistencies. fn rewind_due_to_sequence_gaps( &mut self, - logs: &Vec<(T, LogMeta)>, + logs: &Vec<(SequenceIndexed, LogMeta)>, all_log_sequences: &HashSet, expected_sequences: &HashSet, expected_sequence_range: &RangeInclusive, @@ -300,7 +300,9 @@ impl BackwardSequenceAwareSyncCursor { } #[async_trait] -impl ContractSyncCursor for BackwardSequenceAwareSyncCursor { +impl ContractSyncCursor + for BackwardSequenceAwareSyncCursor +{ async fn next_action(&mut self) -> Result<(CursorAction, Duration)> { // TODO: Fix ETA calculation let eta = Duration::from_secs(0); @@ -327,7 +329,11 @@ impl ContractSyncCursor for BackwardSequenceAwareSyncCu /// ## logs /// The logs to ingest. If any logs are duplicated or their sequence is higher than the current indexing snapshot, /// they are filtered out. - async fn update(&mut self, logs: Vec<(T, LogMeta)>, range: RangeInclusive) -> Result<()> { + async fn update( + &mut self, + logs: Vec<(Indexed, LogMeta)>, + range: RangeInclusive, + ) -> Result<()> { let Some(current_indexing_snapshot) = self.current_indexing_snapshot.clone() else { // We're synced, no need to update at all. return Ok(()); @@ -335,16 +341,15 @@ impl ContractSyncCursor for BackwardSequenceAwareSyncCu // Remove any duplicates, filter out any logs with a higher sequence than our // current snapshot, and sort in ascending order. - let logs = logs + let logs = indexed_to_sequence_indexed_array(logs)? .into_iter() - .unique_by(|(log, _)| log.sequence()) - .filter(|(log, _)| log.sequence() <= current_indexing_snapshot.sequence) - .sorted_by(|(log_a, _), (log_b, _)| log_a.sequence().cmp(&log_b.sequence())) + .unique_by(|(log, _)| log.sequence) + .filter(|(log, _)| log.sequence <= current_indexing_snapshot.sequence) + .sorted_by_key(|(log, _)| log.sequence) .collect::>(); - let all_log_sequences = logs .iter() - .map(|(log, _)| log.sequence()) + .map(|(log, _)| log.sequence) .collect::>(); match &self.index_mode { @@ -448,9 +453,9 @@ mod test { cursor .update( vec![ - (MockSequencedData::new(97), log_meta_with_block(970)), - (MockSequencedData::new(98), log_meta_with_block(980)), - (MockSequencedData::new(99), log_meta_with_block(990)), + (MockSequencedData::new(97).into(), log_meta_with_block(970)), + (MockSequencedData::new(98).into(), log_meta_with_block(980)), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), ], expected_range, ) @@ -515,10 +520,10 @@ mod test { cursor .update( vec![ - (MockSequencedData::new(96), log_meta_with_block(850)), - (MockSequencedData::new(97), log_meta_with_block(860)), - (MockSequencedData::new(98), log_meta_with_block(870)), - (MockSequencedData::new(99), log_meta_with_block(880)), + (MockSequencedData::new(96).into(), log_meta_with_block(850)), + (MockSequencedData::new(97).into(), log_meta_with_block(860)), + (MockSequencedData::new(98).into(), log_meta_with_block(870)), + (MockSequencedData::new(99).into(), log_meta_with_block(880)), ], expected_range, ) @@ -549,7 +554,7 @@ mod test { async fn update_and_expect_rewind( cur: &mut BackwardSequenceAwareSyncCursor, - logs: Vec<(MockSequencedData, LogMeta)>, + logs: Vec<(Indexed, LogMeta)>, ) { // For a more rigorous test case, first do a range where no logs are found, // then in the next range there are issues, and we should rewind to the last indexed snapshot. @@ -610,9 +615,9 @@ mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(96), log_meta_with_block(850)), - (MockSequencedData::new(97), log_meta_with_block(860)), - (MockSequencedData::new(98), log_meta_with_block(870)), + (MockSequencedData::new(96).into(), log_meta_with_block(850)), + (MockSequencedData::new(97).into(), log_meta_with_block(860)), + (MockSequencedData::new(98).into(), log_meta_with_block(870)), ], ) .await; @@ -621,9 +626,9 @@ mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(96), log_meta_with_block(850)), - (MockSequencedData::new(97), log_meta_with_block(860)), - (MockSequencedData::new(99), log_meta_with_block(890)), + (MockSequencedData::new(96).into(), log_meta_with_block(850)), + (MockSequencedData::new(97).into(), log_meta_with_block(860)), + (MockSequencedData::new(99).into(), log_meta_with_block(890)), ], ) .await; @@ -646,10 +651,13 @@ mod test { cursor .update( vec![ - (MockSequencedData::new(99), log_meta_with_block(990)), - (MockSequencedData::new(99), log_meta_with_block(990)), - (MockSequencedData::new(100), log_meta_with_block(1000)), - (MockSequencedData::new(99), log_meta_with_block(990)), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), + ( + MockSequencedData::new(100).into(), + log_meta_with_block(1000), + ), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), ], expected_range, ) @@ -690,7 +698,7 @@ mod test { (0..=99) .map(|i| { ( - MockSequencedData::new(i), + MockSequencedData::new(i).into(), log_meta_with_block(900 + i as u64), ) }) @@ -788,15 +796,15 @@ mod test { cursor .update( vec![ - (MockSequencedData::new(95), log_meta_with_block(950)), - (MockSequencedData::new(96), log_meta_with_block(960)), - (MockSequencedData::new(97), log_meta_with_block(970)), + (MockSequencedData::new(95).into(), log_meta_with_block(950)), + (MockSequencedData::new(96).into(), log_meta_with_block(960)), + (MockSequencedData::new(97).into(), log_meta_with_block(970)), // Add a duplicate here - (MockSequencedData::new(98), log_meta_with_block(980)), - (MockSequencedData::new(98), log_meta_with_block(980)), - (MockSequencedData::new(99), log_meta_with_block(990)), + (MockSequencedData::new(98).into(), log_meta_with_block(980)), + (MockSequencedData::new(98).into(), log_meta_with_block(980)), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), // Put this out of order - (MockSequencedData::new(94), log_meta_with_block(940)), + (MockSequencedData::new(94).into(), log_meta_with_block(940)), ], expected_range, ) @@ -859,7 +867,7 @@ mod test { async fn update_and_expect_rewind( cur: &mut BackwardSequenceAwareSyncCursor, - logs: Vec<(MockSequencedData, LogMeta)>, + logs: Vec<(Indexed, LogMeta)>, ) { // Expect the range to be: // (current - chunk_size, current) @@ -891,10 +899,10 @@ mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(94), log_meta_with_block(940)), - (MockSequencedData::new(95), log_meta_with_block(950)), - (MockSequencedData::new(96), log_meta_with_block(960)), - (MockSequencedData::new(98), log_meta_with_block(980)), + (MockSequencedData::new(94).into(), log_meta_with_block(940)), + (MockSequencedData::new(95).into(), log_meta_with_block(950)), + (MockSequencedData::new(96).into(), log_meta_with_block(960)), + (MockSequencedData::new(98).into(), log_meta_with_block(980)), ], ) .await; @@ -903,11 +911,11 @@ mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(94), log_meta_with_block(940)), - (MockSequencedData::new(95), log_meta_with_block(950)), - (MockSequencedData::new(96), log_meta_with_block(960)), - (MockSequencedData::new(98), log_meta_with_block(980)), - (MockSequencedData::new(99), log_meta_with_block(990)), + (MockSequencedData::new(94).into(), log_meta_with_block(940)), + (MockSequencedData::new(95).into(), log_meta_with_block(950)), + (MockSequencedData::new(96).into(), log_meta_with_block(960)), + (MockSequencedData::new(98).into(), log_meta_with_block(980)), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), ], ) .await; @@ -916,11 +924,11 @@ mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(95), log_meta_with_block(950)), - (MockSequencedData::new(96), log_meta_with_block(960)), - (MockSequencedData::new(97), log_meta_with_block(970)), - (MockSequencedData::new(98), log_meta_with_block(980)), - (MockSequencedData::new(99), log_meta_with_block(990)), + (MockSequencedData::new(95).into(), log_meta_with_block(950)), + (MockSequencedData::new(96).into(), log_meta_with_block(960)), + (MockSequencedData::new(97).into(), log_meta_with_block(970)), + (MockSequencedData::new(98).into(), log_meta_with_block(980)), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), ], ) .await; @@ -929,13 +937,13 @@ mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(93), log_meta_with_block(940)), - (MockSequencedData::new(94), log_meta_with_block(950)), - (MockSequencedData::new(95), log_meta_with_block(950)), - (MockSequencedData::new(96), log_meta_with_block(960)), - (MockSequencedData::new(97), log_meta_with_block(970)), - (MockSequencedData::new(98), log_meta_with_block(980)), - (MockSequencedData::new(99), log_meta_with_block(990)), + (MockSequencedData::new(93).into(), log_meta_with_block(940)), + (MockSequencedData::new(94).into(), log_meta_with_block(950)), + (MockSequencedData::new(95).into(), log_meta_with_block(950)), + (MockSequencedData::new(96).into(), log_meta_with_block(960)), + (MockSequencedData::new(97).into(), log_meta_with_block(970)), + (MockSequencedData::new(98).into(), log_meta_with_block(980)), + (MockSequencedData::new(99).into(), log_meta_with_block(990)), ], ) .await; @@ -961,7 +969,7 @@ mod test { (0..=99) .map(|i| { ( - MockSequencedData::new(i), + MockSequencedData::new(i).into(), log_meta_with_block(900 + i as u64), ) }) diff --git a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs index 204557adb3..aef515b2b6 100644 --- a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs +++ b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs @@ -8,8 +8,9 @@ use std::{ use async_trait::async_trait; use eyre::Result; use hyperlane_core::{ - ContractSyncCursor, CursorAction, HyperlaneSequenceAwareIndexerStoreReader, IndexMode, LogMeta, - SequenceAwareIndexer, Sequenced, + indexed_to_sequence_indexed_array, ContractSyncCursor, CursorAction, + HyperlaneSequenceAwareIndexerStoreReader, IndexMode, Indexed, LogMeta, SequenceAwareIndexer, + SequenceIndexed, }; use itertools::Itertools; use tracing::{debug, warn}; @@ -41,7 +42,7 @@ pub(crate) struct ForwardSequenceAwareSyncCursor { index_mode: IndexMode, } -impl ForwardSequenceAwareSyncCursor { +impl ForwardSequenceAwareSyncCursor { pub fn new( chunk_size: u32, latest_sequence_querier: Arc>, @@ -227,7 +228,7 @@ impl ForwardSequenceAwareSyncCursor { /// - If the target block is reached and the target sequence hasn't been reached, the cursor rewinds to the last indexed snapshot. fn update_block_range( &mut self, - logs: Vec<(T, LogMeta)>, + logs: Vec<(SequenceIndexed, LogMeta)>, all_log_sequences: &HashSet, range: RangeInclusive, ) -> Result<()> { @@ -252,7 +253,7 @@ impl ForwardSequenceAwareSyncCursor { if let Some(highest_sequence_log) = logs.last() { // Update the last indexed snapshot. self.last_indexed_snapshot = LastIndexedSnapshot { - sequence: Some(highest_sequence_log.0.sequence()), + sequence: Some(highest_sequence_log.0.sequence), at_block: highest_sequence_log.1.block_number.try_into()?, }; } @@ -300,7 +301,7 @@ impl ForwardSequenceAwareSyncCursor { /// - If there are any gaps, the cursor rewinds and the range will be retried. fn update_sequence_range( &mut self, - logs: Vec<(T, LogMeta)>, + logs: Vec<(SequenceIndexed, LogMeta)>, all_log_sequences: &HashSet, range: RangeInclusive, ) -> Result<()> { @@ -345,7 +346,7 @@ impl ForwardSequenceAwareSyncCursor { // Update the last indexed snapshot. self.last_indexed_snapshot = LastIndexedSnapshot { - sequence: Some(highest_sequence_log.0.sequence()), + sequence: Some(highest_sequence_log.0.sequence), at_block: highest_sequence_log.1.block_number.try_into()?, }; // Position the current snapshot to the next sequence. @@ -358,7 +359,7 @@ impl ForwardSequenceAwareSyncCursor { /// and logs the inconsistencies due to sequence gaps. fn rewind_due_to_sequence_gaps( &mut self, - logs: &Vec<(T, LogMeta)>, + logs: &Vec<(SequenceIndexed, LogMeta)>, all_log_sequences: &HashSet, expected_sequences: &HashSet, expected_sequence_range: &RangeInclusive, @@ -386,7 +387,9 @@ impl ForwardSequenceAwareSyncCursor { } #[async_trait] -impl ContractSyncCursor for ForwardSequenceAwareSyncCursor { +impl ContractSyncCursor + for ForwardSequenceAwareSyncCursor +{ async fn next_action(&mut self) -> Result<(CursorAction, Duration)> { // TODO: Fix ETA calculation let eta = Duration::from_secs(0); @@ -417,19 +420,23 @@ impl ContractSyncCursor for ForwardSequenceAwareSyncCur /// - Even if the logs include a gap, in practice these logs will have already been inserted into the DB. /// This means that while gaps result in a rewind here, already known logs may be "fast forwarded" through, /// and the cursor won't actually end up re-indexing already known logs. - async fn update(&mut self, logs: Vec<(T, LogMeta)>, range: RangeInclusive) -> Result<()> { + async fn update( + &mut self, + logs: Vec<(Indexed, LogMeta)>, + range: RangeInclusive, + ) -> Result<()> { // Remove any sequence duplicates, filter out any logs preceding our current snapshot, // and sort in ascending order. - let logs = logs + let logs = indexed_to_sequence_indexed_array(logs)? .into_iter() - .unique_by(|(log, _)| log.sequence()) - .filter(|(log, _)| log.sequence() >= self.current_indexing_snapshot.sequence) - .sorted_by(|(log_a, _), (log_b, _)| log_a.sequence().cmp(&log_b.sequence())) + .unique_by(|(log, _)| log.sequence) + .filter(|(log, _)| log.sequence >= self.current_indexing_snapshot.sequence) + .sorted_by(|(log_a, _), (log_b, _)| log_a.sequence.cmp(&log_b.sequence)) .collect::>(); let all_log_sequences = logs .iter() - .map(|(log, _)| log.sequence()) + .map(|(log, _)| log.sequence) .collect::>(); match &self.index_mode { @@ -443,7 +450,7 @@ impl ContractSyncCursor for ForwardSequenceAwareSyncCur #[cfg(test)] pub(crate) mod test { use derive_new::new; - use hyperlane_core::{ChainResult, HyperlaneLogStore, Indexer}; + use hyperlane_core::{ChainResult, HyperlaneLogStore, Indexed, Indexer, Sequenced}; use super::*; @@ -468,7 +475,10 @@ pub(crate) mod test { where T: Sequenced + Debug, { - async fn fetch_logs(&self, _range: RangeInclusive) -> ChainResult> { + async fn fetch_logs( + &self, + _range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { Ok(vec![]) } @@ -484,7 +494,7 @@ pub(crate) mod test { #[async_trait] impl HyperlaneLogStore for MockHyperlaneSequenceAwareIndexerStore { - async fn store_logs(&self, logs: &[(T, LogMeta)]) -> eyre::Result { + async fn store_logs(&self, logs: &[(Indexed, LogMeta)]) -> eyre::Result { Ok(logs.len() as u32) } } @@ -497,7 +507,7 @@ pub(crate) mod test { Ok(self .logs .iter() - .find(|(log, _)| log.sequence() == sequence) + .find(|(log, _)| log.sequence() == Some(sequence)) .map(|(log, _)| log.clone())) } @@ -508,7 +518,7 @@ pub(crate) mod test { Ok(self .logs .iter() - .find(|(log, _)| log.sequence() == sequence) + .find(|(log, _)| log.sequence() == Some(sequence)) .map(|(_, meta)| meta.block_number)) } } @@ -518,9 +528,16 @@ pub(crate) mod test { pub sequence: u32, } + impl Into> for MockSequencedData { + fn into(self) -> Indexed { + let sequence = self.sequence; + Indexed::new(self).with_sequence(sequence) + } + } + impl Sequenced for MockSequencedData { - fn sequence(&self) -> u32 { - self.sequence + fn sequence(&self) -> Option { + Some(self.sequence) } } @@ -645,7 +662,10 @@ pub(crate) mod test { // Update the cursor with the found log. cursor .update( - vec![(MockSequencedData::new(5), log_meta_with_block(115))], + vec![( + Indexed::new(MockSequencedData::new(5)).with_sequence(5), + log_meta_with_block(115), + )], expected_range, ) .await @@ -722,7 +742,10 @@ pub(crate) mod test { // Update the cursor with the found log. cursor .update( - vec![(MockSequencedData::new(5), log_meta_with_block(195))], + vec![( + Indexed::new(MockSequencedData::new(5)).with_sequence(5), + log_meta_with_block(195), + )], expected_range, ) .await @@ -831,7 +854,7 @@ pub(crate) mod test { async fn update_and_expect_rewind( cur: &mut ForwardSequenceAwareSyncCursor, - logs: Vec<(MockSequencedData, LogMeta)>, + logs: Vec<(Indexed, LogMeta)>, ) { // For a more rigorous test case, first do a range where no logs are found, // then in the next range there are issues, and we should rewind to the last indexed snapshot. @@ -891,7 +914,7 @@ pub(crate) mod test { // We don't build upon the last sequence (5 missing) update_and_expect_rewind( &mut cursor, - vec![(MockSequencedData::new(6), log_meta_with_block(100))], + vec![(MockSequencedData::new(6).into(), log_meta_with_block(100))], ) .await; @@ -899,8 +922,8 @@ pub(crate) mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(5), log_meta_with_block(95)), - (MockSequencedData::new(7), log_meta_with_block(105)), + (MockSequencedData::new(5).into(), log_meta_with_block(95)), + (MockSequencedData::new(7).into(), log_meta_with_block(105)), ], ) .await; @@ -933,11 +956,11 @@ pub(crate) mod test { cursor .update( vec![ - (MockSequencedData::new(4), log_meta_with_block(90)), - (MockSequencedData::new(5), log_meta_with_block(95)), - (MockSequencedData::new(5), log_meta_with_block(95)), - (MockSequencedData::new(6), log_meta_with_block(100)), - (MockSequencedData::new(5), log_meta_with_block(95)), + (MockSequencedData::new(4).into(), log_meta_with_block(90)), + (MockSequencedData::new(5).into(), log_meta_with_block(95)), + (MockSequencedData::new(5).into(), log_meta_with_block(95)), + (MockSequencedData::new(6).into(), log_meta_with_block(100)), + (MockSequencedData::new(5).into(), log_meta_with_block(95)), ], expected_range, ) @@ -1019,7 +1042,7 @@ pub(crate) mod test { // Update the cursor with the found log. cursor .update( - vec![(MockSequencedData::new(5), log_meta_with_block(115))], + vec![(MockSequencedData::new(5).into(), log_meta_with_block(115))], expected_range, ) .await @@ -1111,7 +1134,7 @@ pub(crate) mod test { async fn update_and_expect_rewind( cur: &mut ForwardSequenceAwareSyncCursor, - logs: Vec<(MockSequencedData, LogMeta)>, + logs: Vec<(Indexed, LogMeta)>, ) { // Expect the range to be: // (new sequence, new sequence) @@ -1144,8 +1167,8 @@ pub(crate) mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(6), log_meta_with_block(100)), - (MockSequencedData::new(7), log_meta_with_block(105)), + (MockSequencedData::new(6).into(), log_meta_with_block(100)), + (MockSequencedData::new(7).into(), log_meta_with_block(105)), ], ) .await; @@ -1154,8 +1177,8 @@ pub(crate) mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(5), log_meta_with_block(115)), - (MockSequencedData::new(7), log_meta_with_block(120)), + (MockSequencedData::new(5).into(), log_meta_with_block(115)), + (MockSequencedData::new(7).into(), log_meta_with_block(120)), ], ) .await; @@ -1164,8 +1187,8 @@ pub(crate) mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(5), log_meta_with_block(115)), - (MockSequencedData::new(6), log_meta_with_block(120)), + (MockSequencedData::new(5).into(), log_meta_with_block(115)), + (MockSequencedData::new(6).into(), log_meta_with_block(120)), ], ) .await; @@ -1174,10 +1197,10 @@ pub(crate) mod test { update_and_expect_rewind( &mut cursor, vec![ - (MockSequencedData::new(5), log_meta_with_block(115)), - (MockSequencedData::new(6), log_meta_with_block(115)), - (MockSequencedData::new(7), log_meta_with_block(120)), - (MockSequencedData::new(8), log_meta_with_block(125)), + (MockSequencedData::new(5).into(), log_meta_with_block(115)), + (MockSequencedData::new(6).into(), log_meta_with_block(115)), + (MockSequencedData::new(7).into(), log_meta_with_block(120)), + (MockSequencedData::new(8).into(), log_meta_with_block(125)), ], ) .await; diff --git a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/mod.rs b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/mod.rs index b99e00fff8..d3abb4384c 100644 --- a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/mod.rs +++ b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/mod.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use eyre::Result; use hyperlane_core::{ ChainCommunicationError, ContractSyncCursor, CursorAction, - HyperlaneSequenceAwareIndexerStoreReader, IndexMode, LogMeta, SequenceAwareIndexer, Sequenced, + HyperlaneSequenceAwareIndexerStoreReader, IndexMode, Indexed, LogMeta, SequenceAwareIndexer, }; use std::ops::RangeInclusive; @@ -68,7 +68,7 @@ pub(crate) struct ForwardBackwardSequenceAwareSyncCursor { last_direction: SyncDirection, } -impl ForwardBackwardSequenceAwareSyncCursor { +impl ForwardBackwardSequenceAwareSyncCursor { /// Construct a new contract sync helper. pub async fn new( latest_sequence_querier: Arc>, @@ -101,7 +101,9 @@ impl ForwardBackwardSequenceAwareSyncCursor { } #[async_trait] -impl ContractSyncCursor for ForwardBackwardSequenceAwareSyncCursor { +impl ContractSyncCursor + for ForwardBackwardSequenceAwareSyncCursor +{ async fn next_action(&mut self) -> Result<(CursorAction, Duration)> { // TODO: Proper ETA for backwards sync let eta = Duration::from_secs(0); @@ -123,7 +125,11 @@ impl ContractSyncCursor for ForwardBackwardSequenceAwar self.forward.latest_queried_block() } - async fn update(&mut self, logs: Vec<(T, LogMeta)>, range: RangeInclusive) -> Result<()> { + async fn update( + &mut self, + logs: Vec<(Indexed, LogMeta)>, + range: RangeInclusive, + ) -> Result<()> { match self.last_direction { SyncDirection::Forward => self.forward.update(logs, range).await, SyncDirection::Backward => self.backward.update(logs, range).await, diff --git a/rust/hyperlane-base/src/contract_sync/mod.rs b/rust/hyperlane-base/src/contract_sync/mod.rs index 1770ee7939..b97e3e5f4b 100644 --- a/rust/hyperlane-base/src/contract_sync/mod.rs +++ b/rust/hyperlane-base/src/contract_sync/mod.rs @@ -8,7 +8,7 @@ use derive_new::new; use hyperlane_core::{ utils::fmt_sync_time, ContractSyncCursor, CursorAction, HyperlaneDomain, HyperlaneLogStore, HyperlaneSequenceAwareIndexerStore, HyperlaneWatermarkedLogStore, Indexer, - SequenceAwareIndexer, Sequenced, + SequenceAwareIndexer, }; pub use metrics::ContractSyncMetrics; use tokio::time::sleep; @@ -166,7 +166,6 @@ where self.db.clone(), index_settings.chunk_size, index_settings.from, - index_settings.mode, ) .await .unwrap(), @@ -192,7 +191,7 @@ pub type SequencedDataContractSync = #[async_trait] impl ContractSyncer for SequencedDataContractSync where - T: Sequenced + Debug + Clone + Eq + Hash, + T: Send + Sync + Debug + Clone + Eq + Hash + 'static, { /// Returns a new cursor to be used for syncing dispatched messages from the indexer async fn cursor(&self, index_settings: IndexSettings) -> Box> { diff --git a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs index 76151b7428..da61a26ce5 100644 --- a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs +++ b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs @@ -1,11 +1,11 @@ use async_trait::async_trait; -use eyre::Result; +use eyre::{bail, Result}; use paste::paste; use tracing::{debug, instrument, trace}; use hyperlane_core::{ GasPaymentKey, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, - HyperlaneSequenceAwareIndexerStoreReader, HyperlaneWatermarkedLogStore, + HyperlaneSequenceAwareIndexerStoreReader, HyperlaneWatermarkedLogStore, Indexed, InterchainGasExpenditure, InterchainGasPayment, InterchainGasPaymentMeta, LogMeta, MerkleTreeInsertion, H256, }; @@ -22,7 +22,8 @@ const MESSAGE_ID: &str = "message_id_"; const MESSAGE_DISPATCHED_BLOCK_NUMBER: &str = "message_dispatched_block_number_"; const MESSAGE: &str = "message_"; const NONCE_PROCESSED: &str = "nonce_processed_"; -const GAS_PAYMENT_FOR_MESSAGE_ID: &str = "gas_payment_for_message_id_v2_"; +const GAS_PAYMENT_BY_SEQUENCE: &str = "gas_payment_by_sequence_"; +const GAS_PAYMENT_FOR_MESSAGE_ID: &str = "gas_payment_sequence_for_message_id_v2_"; const GAS_PAYMENT_META_PROCESSED: &str = "gas_payment_meta_processed_v3_"; const GAS_EXPENDITURE_FOR_MESSAGE_ID: &str = "gas_expenditure_for_message_id_v2_"; const PENDING_MESSAGE_RETRY_COUNT_FOR_MESSAGE_ID: &str = @@ -107,6 +108,38 @@ impl HyperlaneRocksDB { } } + /// If the provided gas payment, identified by its metadata, has not been + /// processed, processes the gas payment and records it as processed. + /// Returns whether the gas payment was processed for the first time. + pub fn process_indexed_gas_payment( + &self, + indexed_payment: Indexed, + log_meta: &LogMeta, + ) -> DbResult { + let payment = *(indexed_payment.inner()); + let gas_processing_successful = self.process_gas_payment(payment, log_meta)?; + + // only store the payment and return early if there's no sequence + let Some(gas_payment_sequence) = indexed_payment.sequence else { + return Ok(gas_processing_successful); + }; + // otherwise store the indexing decorator as well + if let Ok(Some(_)) = self.retrieve_gas_payment_by_sequence(&gas_payment_sequence) { + trace!( + ?indexed_payment, + ?log_meta, + "Attempted to process an already-processed indexed gas payment" + ); + // Return false to indicate the gas payment was already processed + return Ok(false); + } + + self.store_gas_payment_by_sequence(&gas_payment_sequence, indexed_payment.inner())?; + self.store_gas_payment_block_by_sequence(&gas_payment_sequence, &log_meta.block_number)?; + + Ok(gas_processing_successful) + } + /// If the provided gas payment, identified by its metadata, has not been /// processed, processes the gas payment and records it as processed. /// Returns whether the gas payment was processed for the first time. @@ -174,10 +207,7 @@ impl HyperlaneRocksDB { /// Update the total gas payment for a message to include gas_payment fn update_gas_payment_by_gas_payment_key(&self, event: InterchainGasPayment) -> DbResult<()> { - let gas_payment_key = GasPaymentKey { - message_id: event.message_id, - destination: event.destination, - }; + let gas_payment_key = event.into(); let existing_payment = self.retrieve_gas_payment_by_gas_payment_key(gas_payment_key)?; let total = existing_payment + event; @@ -233,10 +263,10 @@ impl HyperlaneRocksDB { impl HyperlaneLogStore for HyperlaneRocksDB { /// Store a list of dispatched messages and their associated metadata. #[instrument(skip_all)] - async fn store_logs(&self, messages: &[(HyperlaneMessage, LogMeta)]) -> Result { + async fn store_logs(&self, messages: &[(Indexed, LogMeta)]) -> Result { let mut stored = 0; for (message, meta) in messages { - let stored_message = self.store_message(message, meta.block_number)?; + let stored_message = self.store_message(message.inner(), meta.block_number)?; if stored_message { stored += 1; } @@ -248,21 +278,39 @@ impl HyperlaneLogStore for HyperlaneRocksDB { } } +async fn store_and_count_new( + store: &HyperlaneRocksDB, + logs: &[(T, LogMeta)], + log_type: &str, + process: impl Fn(&HyperlaneRocksDB, T, &LogMeta) -> DbResult, +) -> Result { + let mut new_logs = 0; + for (log, meta) in logs { + if process(store, *log, meta)? { + new_logs += 1; + } + } + if new_logs > 0 { + debug!(new_logs, log_type, "Wrote new logs to database"); + } + Ok(new_logs) +} + #[async_trait] impl HyperlaneLogStore for HyperlaneRocksDB { /// Store a list of interchain gas payments and their associated metadata. #[instrument(skip_all)] - async fn store_logs(&self, payments: &[(InterchainGasPayment, LogMeta)]) -> Result { - let mut new = 0; - for (payment, meta) in payments { - if self.process_gas_payment(*payment, meta)? { - new += 1; - } - } - if new > 0 { - debug!(payments = new, "Wrote new gas payments to database"); - } - Ok(new) + async fn store_logs( + &self, + payments: &[(Indexed, LogMeta)], + ) -> Result { + store_and_count_new( + self, + payments, + "gas payments", + HyperlaneRocksDB::process_indexed_gas_payment, + ) + .await } } @@ -270,10 +318,10 @@ impl HyperlaneLogStore for HyperlaneRocksDB { impl HyperlaneLogStore for HyperlaneRocksDB { /// Store every tree insertion event #[instrument(skip_all)] - async fn store_logs(&self, leaves: &[(MerkleTreeInsertion, LogMeta)]) -> Result { + async fn store_logs(&self, leaves: &[(Indexed, LogMeta)]) -> Result { let mut insertions = 0; for (insertion, meta) in leaves { - if self.process_tree_insertion(insertion, meta.block_number)? { + if self.process_tree_insertion(insertion.inner(), meta.block_number)? { insertions += 1; } } @@ -315,28 +363,24 @@ impl HyperlaneSequenceAwareIndexerStoreReader for Hyperlane #[async_trait] impl HyperlaneSequenceAwareIndexerStoreReader for HyperlaneRocksDB { /// Gets data by its sequence. - async fn retrieve_by_sequence(&self, _sequence: u32) -> Result> { - Ok(None) + async fn retrieve_by_sequence(&self, sequence: u32) -> Result> { + Ok(self.retrieve_gas_payment_by_sequence(&sequence)?) } /// Gets the block number at which the log occurred. - async fn retrieve_log_block_number_by_sequence(&self, _sequence: u32) -> Result> { - Ok(None) + async fn retrieve_log_block_number_by_sequence(&self, sequence: u32) -> Result> { + Ok(self.retrieve_gas_payment_block_by_sequence(&sequence)?) } } -/// Note that for legacy reasons this watermark may be shared across multiple cursors, some of which may not have anything to do with gas payments -/// The high watermark cursor is relatively conservative in writing block numbers, so this shouldn't result in any events being missed. #[async_trait] -impl HyperlaneWatermarkedLogStore for HyperlaneRocksDB -where - HyperlaneRocksDB: HyperlaneLogStore, -{ +impl HyperlaneWatermarkedLogStore for HyperlaneRocksDB { /// Gets the block number high watermark async fn retrieve_high_watermark(&self) -> Result> { let watermark = self.retrieve_decodable("", LATEST_INDEXED_GAS_PAYMENT_BLOCK)?; Ok(watermark) } + /// Stores the block number high watermark async fn store_high_watermark(&self, block_number: u32) -> Result<()> { let result = self.store_encodable("", LATEST_INDEXED_GAS_PAYMENT_BLOCK, &block_number)?; @@ -344,6 +388,34 @@ where } } +// Keep this implementation for type compatibility with the `contract_syncs` sync builder +#[async_trait] +impl HyperlaneWatermarkedLogStore for HyperlaneRocksDB { + /// Gets the block number high watermark + async fn retrieve_high_watermark(&self) -> Result> { + bail!("Not implemented") + } + + /// Stores the block number high watermark + async fn store_high_watermark(&self, _block_number: u32) -> Result<()> { + bail!("Not implemented") + } +} + +// Keep this implementation for type compatibility with the `contract_syncs` sync builder +#[async_trait] +impl HyperlaneWatermarkedLogStore for HyperlaneRocksDB { + /// Gets the block number high watermark + async fn retrieve_high_watermark(&self) -> Result> { + bail!("Not implemented") + } + + /// Stores the block number high watermark + async fn store_high_watermark(&self, _block_number: u32) -> Result<()> { + bail!("Not implemented") + } +} + /// Generate a call to ChainSetup for the given builder macro_rules! make_store_and_retrieve { ($vis:vis, $name_suffix:ident, $key_prefix: ident, $key_ty:ty, $val_ty:ty$(,)?) => { @@ -377,6 +449,8 @@ make_store_and_retrieve!(pub, processed_by_nonce, NONCE_PROCESSED, u32, bool); make_store_and_retrieve!(pub(self), processed_by_gas_payment_meta, GAS_PAYMENT_META_PROCESSED, InterchainGasPaymentMeta, bool); make_store_and_retrieve!(pub(self), interchain_gas_expenditure_data_by_message_id, GAS_EXPENDITURE_FOR_MESSAGE_ID, H256, InterchainGasExpenditureData); make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_gas_payment_key, GAS_PAYMENT_FOR_MESSAGE_ID, GasPaymentKey, InterchainGasPaymentData); +make_store_and_retrieve!(pub(self), gas_payment_by_sequence, GAS_PAYMENT_BY_SEQUENCE, u32, InterchainGasPayment); +make_store_and_retrieve!(pub(self), gas_payment_block_by_sequence, GAS_PAYMENT_BY_SEQUENCE, u32, u64); make_store_and_retrieve!( pub, pending_message_retry_count_by_message_id, diff --git a/rust/hyperlane-base/src/db/rocks/test_utils.rs b/rust/hyperlane-base/src/db/rocks/test_utils.rs index 3b4639bbd1..21f45cdfe0 100644 --- a/rust/hyperlane-base/src/db/rocks/test_utils.rs +++ b/rust/hyperlane-base/src/db/rocks/test_utils.rs @@ -31,8 +31,8 @@ where #[cfg(test)] mod test { use hyperlane_core::{ - HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, LogMeta, RawHyperlaneMessage, H256, - H512, U256, + HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, Indexed, LogMeta, + RawHyperlaneMessage, H256, H512, U256, }; use crate::db::HyperlaneRocksDB; @@ -65,7 +65,9 @@ mod test { log_index: U256::from(0), }; - db.store_logs(&vec![(m.clone(), meta)]).await.unwrap(); + db.store_logs(&vec![(Indexed::new(m.clone()), meta)]) + .await + .unwrap(); let by_nonce = db.retrieve_message_by_nonce(m.nonce).unwrap().unwrap(); assert_eq!( diff --git a/rust/hyperlane-base/src/settings/base.rs b/rust/hyperlane-base/src/settings/base.rs index 71edb198fc..59b8fa11a0 100644 --- a/rust/hyperlane-base/src/settings/base.rs +++ b/rust/hyperlane-base/src/settings/base.rs @@ -5,7 +5,7 @@ use futures_util::future::try_join_all; use hyperlane_core::{ HyperlaneChain, HyperlaneDomain, HyperlaneLogStore, HyperlaneProvider, HyperlaneSequenceAwareIndexerStoreReader, HyperlaneWatermarkedLogStore, InterchainGasPaymaster, - Mailbox, MerkleTreeHook, MultisigIsm, SequenceAwareIndexer, Sequenced, ValidatorAnnounce, H256, + Mailbox, MerkleTreeHook, MultisigIsm, SequenceAwareIndexer, ValidatorAnnounce, H256, }; use crate::{ @@ -160,7 +160,7 @@ impl Settings { db: Arc, ) -> eyre::Result>> where - T: Sequenced + Debug, + T: Debug, SequenceIndexer: TryFromWithMetrics, D: HyperlaneLogStore + HyperlaneSequenceAwareIndexerStoreReader + 'static, { @@ -210,7 +210,7 @@ impl Settings { dbs: HashMap>, ) -> Result>>> where - T: Indexable + Sequenced + Debug + Send + Sync + Clone + Eq + Hash + 'static, + T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static, SequenceIndexer: TryFromWithMetrics, D: HyperlaneLogStore + HyperlaneSequenceAwareIndexerStoreReader diff --git a/rust/hyperlane-base/src/settings/chains.rs b/rust/hyperlane-base/src/settings/chains.rs index 1d2c4eded7..f2c4e744b4 100644 --- a/rust/hyperlane-base/src/settings/chains.rs +++ b/rust/hyperlane-base/src/settings/chains.rs @@ -7,8 +7,8 @@ use eyre::{eyre, Context, Result}; use ethers_prometheus::middleware::{ChainInfo, ContractInfo, PrometheusMiddlewareConf}; use hyperlane_core::{ - AggregationIsm, CcipReadIsm, ContractLocator, HyperlaneAbi, HyperlaneDomain, - HyperlaneDomainProtocol, HyperlaneMessage, HyperlaneProvider, IndexMode, + config::OperationBatchConfig, AggregationIsm, CcipReadIsm, ContractLocator, HyperlaneAbi, + HyperlaneDomain, HyperlaneDomainProtocol, HyperlaneMessage, HyperlaneProvider, IndexMode, InterchainGasPaymaster, InterchainGasPayment, InterchainSecurityModule, Mailbox, MerkleTreeHook, MerkleTreeInsertion, MultisigIsm, RoutingIsm, SequenceAwareIndexer, ValidatorAnnounce, H256, @@ -125,6 +125,16 @@ impl ChainConnectionConf { Self::Cosmos(_) => HyperlaneDomainProtocol::Cosmos, } } + + /// Get the message batch configuration for this chain. + pub fn operation_batch_config(&self) -> Option<&OperationBatchConfig> { + match self { + Self::Ethereum(conf) => Some(&conf.operation_batch), + Self::Cosmos(conf) => Some(&conf.operation_batch), + Self::Sealevel(conf) => Some(&conf.operation_batch), + _ => None, + } + } } /// Addresses for mailbox chain contracts diff --git a/rust/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/hyperlane-base/src/settings/parser/connection_parser.rs index 8c256535fd..c7b105c2e5 100644 --- a/rust/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/hyperlane-base/src/settings/parser/connection_parser.rs @@ -1,6 +1,6 @@ use eyre::eyre; use h_eth::TransactionOverrides; -use hyperlane_core::config::ConfigErrResultExt; +use hyperlane_core::config::{ConfigErrResultExt, OperationBatchConfig}; use hyperlane_core::{config::ConfigParsingError, HyperlaneDomainProtocol}; use url::Url; @@ -14,6 +14,7 @@ pub fn build_ethereum_connection_conf( chain: &ValueParser, err: &mut ConfigParsingError, default_rpc_consensus_type: &str, + operation_batch: OperationBatchConfig, ) -> Option { let Some(first_url) = rpcs.to_owned().clone().into_iter().next() else { return None; @@ -67,6 +68,7 @@ pub fn build_ethereum_connection_conf( Some(ChainConnectionConf::Ethereum(h_eth::ConnectionConf { rpc_connection: rpc_connection_conf?, transaction_overrides, + operation_batch, })) } @@ -74,6 +76,7 @@ pub fn build_cosmos_connection_conf( rpcs: &[Url], chain: &ValueParser, err: &mut ConfigParsingError, + operation_batch: OperationBatchConfig, ) -> Option { let mut local_err = ConfigParsingError::default(); let grpcs = @@ -143,6 +146,7 @@ pub fn build_cosmos_connection_conf( canonical_asset.unwrap(), gas_price.unwrap(), contract_address_bytes.unwrap().try_into().unwrap(), + operation_batch, ))) } } @@ -153,18 +157,28 @@ pub fn build_connection_conf( chain: &ValueParser, err: &mut ConfigParsingError, default_rpc_consensus_type: &str, + operation_batch: OperationBatchConfig, ) -> Option { match domain_protocol { - HyperlaneDomainProtocol::Ethereum => { - build_ethereum_connection_conf(rpcs, chain, err, default_rpc_consensus_type) - } + HyperlaneDomainProtocol::Ethereum => build_ethereum_connection_conf( + rpcs, + chain, + err, + default_rpc_consensus_type, + operation_batch, + ), HyperlaneDomainProtocol::Fuel => rpcs .iter() .next() .map(|url| ChainConnectionConf::Fuel(h_fuel::ConnectionConf { url: url.clone() })), HyperlaneDomainProtocol::Sealevel => rpcs.iter().next().map(|url| { - ChainConnectionConf::Sealevel(h_sealevel::ConnectionConf { url: url.clone() }) + ChainConnectionConf::Sealevel(h_sealevel::ConnectionConf { + url: url.clone(), + operation_batch, + }) }), - HyperlaneDomainProtocol::Cosmos => build_cosmos_connection_conf(rpcs, chain, err), + HyperlaneDomainProtocol::Cosmos => { + build_cosmos_connection_conf(rpcs, chain, err, operation_batch) + } } } diff --git a/rust/hyperlane-base/src/settings/parser/mod.rs b/rust/hyperlane-base/src/settings/parser/mod.rs index a13a710dd2..30430e04ec 100644 --- a/rust/hyperlane-base/src/settings/parser/mod.rs +++ b/rust/hyperlane-base/src/settings/parser/mod.rs @@ -187,6 +187,18 @@ fn parse_chain( .parse_address_hash() .end(); + let batch_contract_address = chain + .chain(&mut err) + .get_opt_key("batchContractAddress") + .parse_address_hash() + .end(); + + let max_batch_size = chain + .chain(&mut err) + .get_opt_key("maxBatchSize") + .parse_u32() + .unwrap_or(1); + cfg_unwrap_all!(&chain.cwp, err: [domain]); let connection = build_connection_conf( domain.domain_protocol(), @@ -194,6 +206,10 @@ fn parse_chain( &chain, &mut err, default_rpc_consensus_type, + OperationBatchConfig { + batch_contract_address, + max_batch_size, + }, ); cfg_unwrap_all!(&chain.cwp, err: [connection, mailbox, interchain_gas_paymaster, validator_announce, merkle_tree_hook]); diff --git a/rust/hyperlane-core/src/config/mod.rs b/rust/hyperlane-core/src/config/mod.rs index 58c27d77b9..a0a29d36d5 100644 --- a/rust/hyperlane-core/src/config/mod.rs +++ b/rust/hyperlane-core/src/config/mod.rs @@ -10,6 +10,8 @@ use eyre::Report; pub use str_or_int::{StrOrInt, StrOrIntParseError}; pub use trait_ext::*; +use crate::H256; + mod config_path; mod str_or_int; mod trait_ext; @@ -20,6 +22,15 @@ pub type ConfigResult = Result; /// A no-op filter type. pub type NoFilter = (); +/// Config for batching messages +#[derive(Debug, Clone, Default)] +pub struct OperationBatchConfig { + /// Optional batch contract address (e.g. Multicall3 on EVM chains) + pub batch_contract_address: Option, + /// Batch size + pub max_batch_size: u32, +} + /// A trait that allows for constructing `Self` from a raw config type. pub trait FromRawConf: Sized where diff --git a/rust/hyperlane-core/src/error.rs b/rust/hyperlane-core/src/error.rs index 3eb1ea5b58..ff4693163f 100644 --- a/rust/hyperlane-core/src/error.rs +++ b/rust/hyperlane-core/src/error.rs @@ -82,6 +82,12 @@ pub enum ChainCommunicationError { /// No signer is available and was required for the operation #[error("Signer unavailable")] SignerUnavailable, + /// Batching transaction failed + #[error("Batching transaction failed")] + BatchingFailed, + /// Cannot submit empty batch + #[error("Cannot submit empty batch")] + BatchIsEmpty, /// Failed to parse strings or integers #[error("Data parsing error {0:?}")] StrOrIntParseError(#[from] StrOrIntParseError), diff --git a/rust/hyperlane-core/src/traits/cursor.rs b/rust/hyperlane-core/src/traits/cursor.rs index 1d052c4fc7..cfe92b8dc4 100644 --- a/rust/hyperlane-core/src/traits/cursor.rs +++ b/rust/hyperlane-core/src/traits/cursor.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use auto_impl::auto_impl; use eyre::Result; -use crate::LogMeta; +use crate::{Indexed, LogMeta}; /// A cursor governs event indexing for a contract. #[async_trait] @@ -24,7 +24,11 @@ pub trait ContractSyncCursor: Send + Sync + 'static { /// This is called after the logs have been written to the store, /// however may require logs to meet certain criteria (e.g. no gaps), that if /// not met, should result in internal state changes (e.g. rewinding) and not an Err. - async fn update(&mut self, logs: Vec<(T, LogMeta)>, range: RangeInclusive) -> Result<()>; + async fn update( + &mut self, + logs: Vec<(Indexed, LogMeta)>, + range: RangeInclusive, + ) -> Result<()>; } /// The action that should be taken by the contract sync loop diff --git a/rust/hyperlane-core/src/traits/db.rs b/rust/hyperlane-core/src/traits/db.rs index 080a182b45..92c0c15f7b 100644 --- a/rust/hyperlane-core/src/traits/db.rs +++ b/rust/hyperlane-core/src/traits/db.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use auto_impl::auto_impl; use eyre::Result; -use crate::LogMeta; +use crate::{Indexed, LogMeta}; /// Interface for a HyperlaneLogStore that ingests logs. #[async_trait] @@ -12,7 +12,7 @@ use crate::LogMeta; pub trait HyperlaneLogStore: Send + Sync + Debug { /// Store a list of logs and their associated metadata /// Returns the number of elements that were stored. - async fn store_logs(&self, logs: &[(T, LogMeta)]) -> Result; + async fn store_logs(&self, logs: &[(Indexed, LogMeta)]) -> Result; } /// A sequence is a monotonically increasing number that is incremented every time a message ID is indexed. @@ -20,7 +20,7 @@ pub trait HyperlaneLogStore: Send + Sync + Debug { /// is equal to the leaf index. pub trait Sequenced: 'static + Send + Sync { /// The sequence of this sequenced type. - fn sequence(&self) -> u32; + fn sequence(&self) -> Option; } /// A read-only interface for a sequence-aware indexer store. diff --git a/rust/hyperlane-core/src/traits/encode.rs b/rust/hyperlane-core/src/traits/encode.rs index bd4b066eba..b03ac92d86 100644 --- a/rust/hyperlane-core/src/traits/encode.rs +++ b/rust/hyperlane-core/src/traits/encode.rs @@ -1,6 +1,8 @@ use std::io::{Error, ErrorKind}; -use crate::{GasPaymentKey, HyperlaneProtocolError, H160, H256, H512, U256}; +use crate::{ + GasPaymentKey, HyperlaneProtocolError, Indexed, InterchainGasPayment, H160, H256, H512, U256, +}; /// Simple trait for types with a canonical encoding pub trait Encode { @@ -204,3 +206,99 @@ impl Decode for GasPaymentKey { }) } } + +impl Encode for InterchainGasPayment { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: std::io::Write, + { + let mut written = 0; + written += self.message_id.write_to(writer)?; + written += self.destination.write_to(writer)?; + written += self.payment.write_to(writer)?; + written += self.gas_amount.write_to(writer)?; + Ok(written) + } +} + +impl Decode for InterchainGasPayment { + fn read_from(reader: &mut R) -> Result + where + R: std::io::Read, + Self: Sized, + { + Ok(Self { + message_id: H256::read_from(reader)?, + destination: u32::read_from(reader)?, + payment: U256::read_from(reader)?, + gas_amount: U256::read_from(reader)?, + }) + } +} + +// TODO: Could generalize this implementation to support encoding arbitrary `Option` +// where T: Encode + Decode +impl Encode for Indexed { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: std::io::Write, + { + let mut written = 0; + written += self.inner().write_to(writer)?; + match self.sequence { + Some(sequence) => { + let sequence_is_defined = true; + written += sequence_is_defined.write_to(writer)?; + written += sequence.write_to(writer)?; + } + None => { + let sequence_is_defined = false; + written += sequence_is_defined.write_to(writer)?; + } + } + Ok(written) + } +} + +impl Decode for Indexed { + fn read_from(reader: &mut R) -> Result + where + R: std::io::Read, + Self: Sized, + { + let inner = T::read_from(reader)?; + let sequence_is_defined = bool::read_from(reader)?; + let mut indexed = Self::new(inner); + if sequence_is_defined { + let sequence = u32::read_from(reader)?; + indexed = indexed.with_sequence(sequence) + } + Ok(indexed) + } +} + +#[cfg(test)] +mod test { + use crate::{Decode, Encode, Indexed, H256}; + + #[test] + fn test_encoding_indexed() { + let indexed: Indexed = Indexed::new(H256::random()).with_sequence(5); + let encoded = indexed.to_vec(); + let decoded = Indexed::::read_from(&mut &encoded[..]).unwrap(); + assert_eq!(indexed, decoded); + } + + #[test] + fn test_encoding_interchain_gas_payment() { + let payment = super::InterchainGasPayment { + message_id: Default::default(), + destination: 42, + payment: 100.into(), + gas_amount: 200.into(), + }; + let encoded = payment.to_vec(); + let decoded = super::InterchainGasPayment::read_from(&mut &encoded[..]).unwrap(); + assert_eq!(payment, decoded); + } +} diff --git a/rust/hyperlane-core/src/traits/indexer.rs b/rust/hyperlane-core/src/traits/indexer.rs index 17bcf97585..3db7e4f570 100644 --- a/rust/hyperlane-core/src/traits/indexer.rs +++ b/rust/hyperlane-core/src/traits/indexer.rs @@ -11,7 +11,7 @@ use async_trait::async_trait; use auto_impl::auto_impl; use serde::Deserialize; -use crate::{ChainResult, LogMeta}; +use crate::{ChainResult, Indexed, LogMeta}; /// Indexing mode. #[derive(Copy, Debug, Default, Deserialize, Clone)] @@ -29,7 +29,10 @@ pub enum IndexMode { #[auto_impl(&, Box, Arc,)] pub trait Indexer: Send + Sync + Debug { /// Fetch list of logs between blocks `from` and `to`, inclusive. - async fn fetch_logs(&self, range: RangeInclusive) -> ChainResult>; + async fn fetch_logs( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>>; /// Get the chain's latest block number that has reached finality async fn get_finalized_block_number(&self) -> ChainResult; diff --git a/rust/hyperlane-core/src/traits/mailbox.rs b/rust/hyperlane-core/src/traits/mailbox.rs index aea7a87395..fbd9dd5bc0 100644 --- a/rust/hyperlane-core/src/traits/mailbox.rs +++ b/rust/hyperlane-core/src/traits/mailbox.rs @@ -2,17 +2,15 @@ use std::fmt::Debug; use std::num::NonZeroU64; use async_trait::async_trait; -use auto_impl::auto_impl; use crate::{ - traits::TxOutcome, utils::domain_hash, ChainResult, HyperlaneContract, HyperlaneMessage, - TxCostEstimate, H256, U256, + traits::TxOutcome, utils::domain_hash, BatchItem, ChainCommunicationError, ChainResult, + HyperlaneContract, HyperlaneMessage, TxCostEstimate, H256, U256, }; /// Interface for the Mailbox chain contract. Allows abstraction over different /// chains #[async_trait] -#[auto_impl(&, Box, Arc)] pub trait Mailbox: HyperlaneContract + Send + Sync + Debug { /// Return the domain hash fn domain_hash(&self) -> H256 { @@ -42,6 +40,15 @@ pub trait Mailbox: HyperlaneContract + Send + Sync + Debug { tx_gas_limit: Option, ) -> ChainResult; + /// Process a message with a proof against the provided signed checkpoint + async fn process_batch( + &self, + _messages: &[BatchItem], + ) -> ChainResult { + // Batching is not supported by default + Err(ChainCommunicationError::BatchingFailed) + } + /// Estimate transaction costs to process a message. async fn process_estimate_costs( &self, diff --git a/rust/hyperlane-core/src/types/indexing.rs b/rust/hyperlane-core/src/types/indexing.rs new file mode 100644 index 0000000000..5a7cfc48e3 --- /dev/null +++ b/rust/hyperlane-core/src/types/indexing.rs @@ -0,0 +1,75 @@ +use derive_new::new; + +use crate::{HyperlaneMessage, MerkleTreeInsertion, Sequenced}; + +/// Wrapper struct that adds indexing information to a type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, new)] +pub struct Indexed { + inner: T, + #[new(default)] + /// Optional sequence data that is useful during indexing + pub sequence: Option, +} + +/// Counterpart of `Indexed` that is sure to have the `sequence` field set +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, new)] +pub struct SequenceIndexed { + inner: T, + /// Sequence data that is useful during indexing + pub sequence: u32, +} + +impl TryFrom> for SequenceIndexed { + type Error = eyre::Report; + + fn try_from(value: Indexed) -> Result { + match value.sequence { + Some(sequence) => Ok(SequenceIndexed::new(value.inner, sequence)), + None => eyre::bail!("Missing indexing sequence"), + } + } +} + +/// Convert a vector of `Indexed` values to a vector of `SequenceIndexed` values +/// so that if any `Option` is `None`, the conversion will fail +pub fn indexed_to_sequence_indexed_array( + indexed_array: Vec<(Indexed, U)>, +) -> Result, U)>, eyre::Report> { + indexed_array + .into_iter() + .map(|(item, meta)| SequenceIndexed::::try_from(item).map(|si| (si, meta))) + .collect() +} + +impl Sequenced for Indexed { + fn sequence(&self) -> Option { + self.sequence + } +} + +impl Indexed { + /// Set the sequence of the indexed value, returning a new instance of `Self` + pub fn with_sequence(mut self, sequence: u32) -> Self { + self.sequence = Some(sequence); + self + } + + /// Get the inner value + pub fn inner(&self) -> &T { + &self.inner + } +} + +impl From for Indexed { + fn from(value: HyperlaneMessage) -> Self { + let nonce = value.nonce; + Indexed::new(value).with_sequence(nonce as _) + } +} + +impl From for Indexed { + fn from(value: MerkleTreeInsertion) -> Self { + let sequence = value.index(); + Indexed::new(value).with_sequence(sequence as _) + } +} diff --git a/rust/hyperlane-core/src/types/merkle_tree.rs b/rust/hyperlane-core/src/types/merkle_tree.rs index 5d4e20a9fc..c64c533a84 100644 --- a/rust/hyperlane-core/src/types/merkle_tree.rs +++ b/rust/hyperlane-core/src/types/merkle_tree.rs @@ -1,7 +1,7 @@ use derive_new::new; use std::io::{Read, Write}; -use crate::{Decode, Encode, HyperlaneProtocolError, Sequenced, H256}; +use crate::{Decode, Encode, HyperlaneProtocolError, H256}; /// Merkle Tree Hook insertion event #[derive(Debug, Copy, Clone, new, Eq, PartialEq, Hash)] @@ -22,12 +22,6 @@ impl MerkleTreeInsertion { } } -impl Sequenced for MerkleTreeInsertion { - fn sequence(&self) -> u32 { - self.leaf_index - } -} - impl Encode for MerkleTreeInsertion { fn write_to(&self, writer: &mut W) -> std::io::Result where diff --git a/rust/hyperlane-core/src/types/message.rs b/rust/hyperlane-core/src/types/message.rs index 99fb2fef45..4a97683e17 100644 --- a/rust/hyperlane-core/src/types/message.rs +++ b/rust/hyperlane-core/src/types/message.rs @@ -2,7 +2,7 @@ use sha3::{digest::Update, Digest, Keccak256}; use std::fmt::{Debug, Display, Formatter}; use crate::utils::{fmt_address_for_domain, fmt_domain}; -use crate::{Decode, Encode, HyperlaneProtocolError, Sequenced, H256}; +use crate::{Decode, Encode, HyperlaneProtocolError, H256}; const HYPERLANE_MESSAGE_PREFIX_LEN: usize = 77; @@ -54,12 +54,6 @@ impl Default for HyperlaneMessage { } } -impl Sequenced for HyperlaneMessage { - fn sequence(&self) -> u32 { - self.nonce - } -} - impl Debug for HyperlaneMessage { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( diff --git a/rust/hyperlane-core/src/types/mod.rs b/rust/hyperlane-core/src/types/mod.rs index 2f348c7224..59f20630bf 100644 --- a/rust/hyperlane-core/src/types/mod.rs +++ b/rust/hyperlane-core/src/types/mod.rs @@ -11,21 +11,25 @@ pub use chain_data::*; #[cfg(feature = "async")] pub use channel::*; pub use checkpoint::*; +pub use indexing::*; pub use log_metadata::*; pub use merkle_tree::*; pub use message::*; +pub use transaction::*; -use crate::{Decode, Encode, HyperlaneProtocolError, Sequenced}; +use crate::{Decode, Encode, HyperlaneProtocolError}; mod announcement; mod chain_data; #[cfg(feature = "async")] mod channel; mod checkpoint; +mod indexing; mod log_metadata; mod merkle_tree; mod message; mod serialize; +mod transaction; /// Unified 32-byte identifier with convenience tooling for handling /// 20-byte ids (e.g ethereum addresses) @@ -118,6 +122,15 @@ pub struct GasPaymentKey { pub destination: u32, } +impl From for GasPaymentKey { + fn from(value: InterchainGasPayment) -> Self { + Self { + message_id: value.message_id, + destination: value.destination, + } + } +} + /// A payment of a message's gas costs. #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] pub struct InterchainGasPayment { @@ -131,12 +144,6 @@ pub struct InterchainGasPayment { pub gas_amount: U256, } -impl Sequenced for InterchainGasPayment { - fn sequence(&self) -> u32 { - Default::default() - } -} - /// Amount of gas spent attempting to send the message. #[derive(Debug, Copy, Clone)] pub struct InterchainGasExpenditure { diff --git a/rust/hyperlane-core/src/types/transaction.rs b/rust/hyperlane-core/src/types/transaction.rs new file mode 100644 index 0000000000..3f6085d659 --- /dev/null +++ b/rust/hyperlane-core/src/types/transaction.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use crate::{ChainResult, Mailbox, U256}; +use derive_new::new; + +/// State for the next submission attempt generated by a prepare call. +#[derive(Clone, Debug)] +pub struct MessageSubmissionData { + /// Transaction metadata - currently only applies to Messages, so this field can be made optional or generic if other + /// operations are submitted in the future. + pub metadata: Vec, + /// Gas limit for the transaction + pub gas_limit: U256, +} + +/// A an item to be batched for submission to the chain. +#[derive(new, Clone, Debug)] +pub struct BatchItem { + /// The data to be submitted + pub data: T, + /// Data to do with this transaction submission + pub submission_data: MessageSubmissionData, + /// The mailbox to send the result to + /// TODO: turn this into a `destination contract` object when we batch more than just messages + pub mailbox: Arc, +} + +/// Need to define a trait instead of using TryInto because the latter is not +/// object safe +pub trait TryBatchAs { + /// Try to convert the item into a batch item + fn try_batch(&self) -> ChainResult> { + Err(crate::ChainCommunicationError::BatchingFailed) + } +} diff --git a/rust/hyperlane-test/src/mocks/mailbox.rs b/rust/hyperlane-test/src/mocks/mailbox.rs index 314778101c..8d93706f80 100644 --- a/rust/hyperlane-test/src/mocks/mailbox.rs +++ b/rust/hyperlane-test/src/mocks/mailbox.rs @@ -93,6 +93,13 @@ impl Mailbox for MockMailboxContract { self.process(message, metadata, tx_gas_limit) } + async fn process_batch( + &self, + messages: &[BatchItem], + ) -> ChainResult { + self.process_batch(messages).await + } + async fn process_estimate_costs( &self, message: &HyperlaneMessage, diff --git a/rust/utils/run-locally/Cargo.toml b/rust/utils/run-locally/Cargo.toml index bf57138f4e..45c07d030d 100644 --- a/rust/utils/run-locally/Cargo.toml +++ b/rust/utils/run-locally/Cargo.toml @@ -22,6 +22,10 @@ serde_json.workspace = true hex.workspace = true ctrlc.workspace = true eyre.workspace = true +ethers.workspace = true +ethers-core.workspace = true +ethers-contract.workspace = true +tokio.workspace = true maplit.workspace = true nix = { workspace = true, features = ["signal"], default-features = false } tempfile.workspace = true diff --git a/rust/utils/run-locally/src/cosmos/mod.rs b/rust/utils/run-locally/src/cosmos/mod.rs index ab14818295..1a3f1e7cdd 100644 --- a/rust/utils/run-locally/src/cosmos/mod.rs +++ b/rust/utils/run-locally/src/cosmos/mod.rs @@ -294,6 +294,8 @@ fn launch_cosmos_relayer( .hyp_env("RELAYCHAINS", relay_chains.join(",")) .hyp_env("DB", relayer_base.as_ref().to_str().unwrap()) .hyp_env("ALLOWLOCALCHECKPOINTSYNCERS", "true") + .hyp_env("CHAINS_COSMOSTEST99990_MAXBATCHSIZE", "5") + .hyp_env("CHAINS_COSMOSTEST99991_MAXBATCHSIZE", "5") .hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" }) .hyp_env("GASPAYMENTENFORCEMENT", "[{\"type\": \"none\"}]") .hyp_env("METRICSPORT", metrics.to_string()) @@ -615,10 +617,11 @@ fn termination_invariants_met( #[cfg(feature = "cosmos")] mod test { - use super::*; #[test] fn test_run() { + use crate::cosmos::run_locally; + run_locally() } } diff --git a/rust/utils/run-locally/src/ethereum.rs b/rust/utils/run-locally/src/ethereum/mod.rs similarity index 58% rename from rust/utils/run-locally/src/ethereum.rs rename to rust/utils/run-locally/src/ethereum/mod.rs index 5785260b32..bebe063484 100644 --- a/rust/utils/run-locally/src/ethereum.rs +++ b/rust/utils/run-locally/src/ethereum/mod.rs @@ -2,14 +2,19 @@ use std::sync::Arc; use std::thread::sleep; use std::time::Duration; +use ethers::providers::{Http, Provider}; +use ethers::types::{H160, H256, U256}; use macro_rules_attribute::apply; use crate::config::Config; +use crate::ethereum::multicall::{DEPLOYER_ADDRESS, SIGNED_DEPLOY_MULTICALL_TX}; use crate::logging::log; use crate::program::Program; use crate::utils::{as_task, AgentHandles, TaskHandle}; use crate::{INFRA_PATH, MONOREPO_ROOT_PATH}; +mod multicall; + #[apply(as_task)] pub fn start_anvil(config: Arc) -> AgentHandles { log!("Installing typescript dependencies..."); @@ -43,5 +48,38 @@ pub fn start_anvil(config: Arc) -> AgentHandles { log!("Deploying hyperlane core contracts..."); yarn_infra.clone().cmd("deploy-core").run().join(); + log!("Deploying multicall contract..."); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(deploy_multicall()); + anvil } + +pub async fn deploy_multicall() { + let anvil_rpc_url = "http://127.0.0.1:8545"; + let provider = Provider::::try_from(anvil_rpc_url) + .unwrap() + .interval(Duration::from_millis(50u64)); + + // fund the deployer address + provider + .request::<(H160, U256), ()>( + "anvil_setBalance", + (DEPLOYER_ADDRESS, U256::from(1_000_000_000_000_000_000u64)), + ) + .await + .unwrap(); + + // deploy multicall + provider + .request::<[serde_json::Value; 1], H256>( + "eth_sendRawTransaction", + [SIGNED_DEPLOY_MULTICALL_TX.into()], + ) + .await + .unwrap(); + log!("Successfully deployed multicall contract..."); +} diff --git a/rust/utils/run-locally/src/ethereum/multicall.rs b/rust/utils/run-locally/src/ethereum/multicall.rs new file mode 100644 index 0000000000..cbad31822d --- /dev/null +++ b/rust/utils/run-locally/src/ethereum/multicall.rs @@ -0,0 +1,10 @@ +use ethers_core::types::H160; + +/// presigned tx that will deploy multicall3 +pub const SIGNED_DEPLOY_MULTICALL_TX: &str = "0xf90f538085174876e800830f42408080b90f00608060405234801561001057600080fd5b50610ee0806100206000396000f3fe6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c00331ca0edce47092c0f398cebf3ffc267f05c8e7076e3b89445e0fe50f6332273d4569ba01b0b9d000e19b24c5869b0fc3b22b0d6fa47cd63316875cbbd577d76e6fde086"; + +/// address to be funded for `SIGNED_DEPLOY_MULTICALL_TX` to succeed +pub const DEPLOYER_ADDRESS: H160 = H160([ + 0x05, 0xf3, 0x2B, 0x3c, 0xC3, 0x88, 0x84, 0x53, 0xff, 0x71, 0xB0, 0x11, 0x35, 0xB3, 0x4F, 0xF8, + 0xe4, 0x12, 0x63, 0xF2, +]); diff --git a/rust/utils/run-locally/src/main.rs b/rust/utils/run-locally/src/main.rs index b4efc6115f..a287b2bd1f 100644 --- a/rust/utils/run-locally/src/main.rs +++ b/rust/utils/run-locally/src/main.rs @@ -21,6 +21,7 @@ use std::{ time::{Duration, Instant}, }; +use ethers_contract::MULTICALL_ADDRESS; use logging::log; pub use metrics::fetch_metric; use program::Program; @@ -157,6 +158,8 @@ fn main() -> ExitCode { .hyp_env("CHAINS_TEST2_INDEX_CHUNK", "1") .hyp_env("CHAINS_TEST3_INDEX_CHUNK", "1"); + let multicall_address_string: String = format!("0x{}", hex::encode(MULTICALL_ADDRESS)); + let relayer_env = common_agent_env .clone() .bin(concat_path(AGENT_BIN_PATH, "relayer")) @@ -165,10 +168,25 @@ fn main() -> ExitCode { "CHAINS_TEST2_CONNECTION_URLS", "http://127.0.0.1:8545,http://127.0.0.1:8545,http://127.0.0.1:8545", ) + .hyp_env( + "CHAINS_TEST1_BATCHCONTRACTADDRESS", + multicall_address_string.clone(), + ) + .hyp_env("CHAINS_TEST1_MAXBATCHSIZE", "5") // by setting this as a quorum provider we will cause nonce errors when delivering to test2 // because the message will be sent to the node 3 times. .hyp_env("CHAINS_TEST2_RPCCONSENSUSTYPE", "quorum") + .hyp_env( + "CHAINS_TEST2_BATCHCONTRACTADDRESS", + multicall_address_string.clone(), + ) + .hyp_env("CHAINS_TEST2_MAXBATCHSIZE", "5") .hyp_env("CHAINS_TEST3_CONNECTION_URL", "http://127.0.0.1:8545") + .hyp_env( + "CHAINS_TEST3_BATCHCONTRACTADDRESS", + multicall_address_string, + ) + .hyp_env("CHAINS_TEST3_MAXBATCHSIZE", "5") .hyp_env("METRICSPORT", "9092") .hyp_env("DB", relayer_db.to_str().unwrap()) .hyp_env("CHAINS_TEST1_SIGNER_KEY", RELAYER_KEYS[0]) diff --git a/solidity/.gas-snapshot b/solidity/.gas-snapshot index 0006920fb1..2eb886c781 100644 --- a/solidity/.gas-snapshot +++ b/solidity/.gas-snapshot @@ -1,91 +1,277 @@ -AggregationIsmTest:testModulesAndThreshold(uint8,uint8,bytes32) (runs: 256, μ: 1556428, ~: 1334785) -AggregationIsmTest:testVerify(uint8,uint8,bytes32) (runs: 256, μ: 1581057, ~: 1358996) -AggregationIsmTest:testVerifyIncorrectMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1580974, ~: 1359683) -AggregationIsmTest:testVerifyMissingMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1573730, ~: 1351998) -AggregationIsmTest:testVerifyNoMetadataRequired(uint8,uint8,uint8,bytes32) (runs: 256, μ: 1722168, ~: 1325061) -DomainRoutingIsmTest:testRoute(uint32,bytes32) (runs: 256, μ: 435158, ~: 435236) -DomainRoutingIsmTest:testSet(uint32) (runs: 256, μ: 421157, ~: 421157) -DomainRoutingIsmTest:testSetManyViaFactory(uint8,uint32) (runs: 256, μ: 40359815, ~: 29407180) -DomainRoutingIsmTest:testSetNonOwner(uint32,address) (runs: 256, μ: 11298, ~: 11298) -DomainRoutingIsmTest:testVerify(uint32,bytes32) (runs: 256, μ: 441253, ~: 441331) -DomainRoutingIsmTest:testVerifyNoIsm(uint32,bytes32) (runs: 256, μ: 444212, ~: 444212) -GasRouterTest:testDispatchWithGas(uint256) (runs: 256, μ: 347585, ~: 347585) -GasRouterTest:testQuoteGasPayment(uint256) (runs: 256, μ: 85818, ~: 85818) -GasRouterTest:testSetDestinationGas(uint256) (runs: 256, μ: 73808, ~: 75985) -InterchainAccountRouterTest:testCallRemoteWithDefault(bytes32) (runs: 256, μ: 555673, ~: 556140) -InterchainAccountRouterTest:testCallRemoteWithFailingDefaultIsm(bytes32) (runs: 256, μ: 601813, ~: 602824) -InterchainAccountRouterTest:testCallRemoteWithFailingIsmOverride(bytes32) (runs: 256, μ: 619172, ~: 620105) -InterchainAccountRouterTest:testCallRemoteWithOverrides(bytes32) (runs: 256, μ: 460466, ~: 460933) -InterchainAccountRouterTest:testCallRemoteWithoutDefaults(bytes32) (runs: 256, μ: 20418, ~: 20418) -InterchainAccountRouterTest:testConstructor() (gas: 2577062) -InterchainAccountRouterTest:testEnrollRemoteRouterAndIsm(bytes32,bytes32) (runs: 256, μ: 109207, ~: 109207) -InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmImmutable(bytes32,bytes32,bytes32,bytes32) (runs: 256, μ: 106926, ~: 106926) -InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmNonOwner(address,bytes32,bytes32) (runs: 256, μ: 20313, ~: 20313) -InterchainAccountRouterTest:testEnrollRemoteRouters(uint8,uint32,bytes32) (runs: 256, μ: 4050183, ~: 3316983) -InterchainAccountRouterTest:testGetLocalInterchainAccount(bytes32) (runs: 256, μ: 468785, ~: 469252) -InterchainAccountRouterTest:testGetRemoteInterchainAccount() (gas: 120253) -InterchainAccountRouterTest:testOverrideAndCallRemote(bytes32) (runs: 256, μ: 555674, ~: 556141) -InterchainAccountRouterTest:testReceiveValue(uint256) (runs: 256, μ: 109672, ~: 109672) -InterchainAccountRouterTest:testSendValue(uint256) (runs: 256, μ: 484762, ~: 484840) -InterchainAccountRouterTest:testSingleCallRemoteWithDefault(bytes32) (runs: 256, μ: 556324, ~: 556791) -InterchainGasPaymasterTest:testClaim() (gas: 90675) -InterchainGasPaymasterTest:testConstructorSetsBeneficiary() (gas: 7648) -InterchainGasPaymasterTest:testGetExchangeRateAndGasPrice() (gas: 41743) -InterchainGasPaymasterTest:testGetExchangeRateAndGasPriceRevertsIfNoGasOracleSet() (gas: 10837) -InterchainGasPaymasterTest:testInitializeRevertsIfCalledTwice() (gas: 11087) -InterchainGasPaymasterTest:testPayForGas() (gas: 92812) -InterchainGasPaymasterTest:testPayForGasRevertsIfPaymentInsufficient() (gas: 44808) -InterchainGasPaymasterTest:testQuoteGasPaymentRemoteVeryCheap() (gas: 41751) -InterchainGasPaymasterTest:testQuoteGasPaymentRemoteVeryExpensive() (gas: 41731) -InterchainGasPaymasterTest:testQuoteGasPaymentRevertsIfNoGasOracleSet() (gas: 10803) -InterchainGasPaymasterTest:testQuoteGasPaymentSimilarExchangeRate() (gas: 41796) -InterchainGasPaymasterTest:testSetBeneficiary() (gas: 18694) -InterchainGasPaymasterTest:testSetBeneficiaryRevertsIfNotOwner() (gas: 11033) -InterchainGasPaymasterTest:testSetGasOracle() (gas: 40459) -InterchainGasPaymasterTest:testSetGasOracleRevertsIfNotOwner() (gas: 13783) -InterchainQueryRouterTest:testCannotCallbackReverting() (gas: 1372627) -InterchainQueryRouterTest:testCannotQueryReverting() (gas: 1094371) -InterchainQueryRouterTest:testQueryAddress(address) (runs: 256, μ: 1383752, ~: 1383830) -InterchainQueryRouterTest:testQueryUint256(uint256) (runs: 256, μ: 1566435, ~: 1567679) -InterchainQueryRouterTest:testSingleQueryAddress(address) (runs: 256, μ: 1383789, ~: 1383867) -LiquidityLayerRouterTest:testCannotSendToRecipientWithoutHandle() (gas: 646167) -LiquidityLayerRouterTest:testDispatchWithTokenTransfersMovesTokens() (gas: 513057) -LiquidityLayerRouterTest:testDispatchWithTokensCallsAdapter() (gas: 519167) -LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithFailedTransferIn() (gas: 29596) -LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithUnkownBridgeAdapter() (gas: 20663) -LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 745139) -LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 578662) -LiquidityLayerRouterTest:testSendToRecipientWithoutHandleWhenSpecifyingNoMessage() (gas: 1161102) -LiquidityLayerRouterTest:testSetLiquidityLayerAdapter() (gas: 23363) -MerkleRootMultisigIsmTest:testFailVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 336856, ~: 330762) -MerkleRootMultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 339564, ~: 331597) -MessageIdMultisigIsmTest:testFailVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 312768, ~: 307238) -MessageIdMultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 314834, ~: 307097) -MessagingTest:testSendMessage(string) (runs: 256, μ: 263049, ~: 278203) -OverheadIgpTest:testDestinationGasAmount() (gas: 33814) -OverheadIgpTest:testDestinationGasAmountWhenOverheadNotSet() (gas: 7912) -OverheadIgpTest:testInnerIgpSet() (gas: 7632) -OverheadIgpTest:testPayForGas() (gas: 65328) -OverheadIgpTest:testQuoteGasPayment() (gas: 42768) -OverheadIgpTest:testSetDestinationGasAmounts() (gas: 63435) -OverheadIgpTest:testSetDestinationGasAmountsNotOwner() (gas: 12018) -PausableReentrancyGuardTest:testNonreentrant() (gas: 9628) -PausableReentrancyGuardTest:testNonreentrantNotPaused() (gas: 14163) -PausableReentrancyGuardTest:testPause() (gas: 13635) -PortalAdapterTest:testAdapter(uint256) (runs: 256, μ: 135466, ~: 135583) -PortalAdapterTest:testReceivingRevertsWithoutTransferCompletion(uint256) (runs: 256, μ: 140405, ~: 140522) -PortalAdapterTest:testReceivingWorks(uint256) (runs: 256, μ: 229401, ~: 229513) -StorageGasOracleTest:testConstructorSetsOwnership() (gas: 7611) -StorageGasOracleTest:testGetExchangeRateAndGasPrice() (gas: 12456) +AggregationHookTest:testHookType() (gas: 319874) +AggregationHookTest:testMetadata(uint8) (runs: 264, μ: 20752667, ~: 7535833) +AggregationHookTest:testPostDispatch(uint8) (runs: 264, μ: 23810393, ~: 8594061) +AggregationHookTest:testPostDispatch_reverts_outOfFund(uint8,uint8) (runs: 259, μ: 37093348, ~: 44184778) +AggregationHookTest:testQuoteDispatch(uint8) (runs: 264, μ: 20867910, ~: 7577493) +AggregationIsmTest:testModulesAndThreshold(uint8,uint8,bytes32) (runs: 258, μ: 1583362, ~: 1364904) +AggregationIsmTest:testVerify(uint8,uint8,bytes32) (runs: 258, μ: 1575553, ~: 1359715) +AggregationIsmTest:testVerifyIncorrectMetadata(uint8,uint8,bytes32) (runs: 258, μ: 1575478, ~: 1360125) +AggregationIsmTest:testVerifyMissingMetadata(uint8,uint8,bytes32) (runs: 258, μ: 1568486, ~: 1352936) +AggregationIsmTest:testVerifyNoMetadataRequired(uint8,uint8,uint8,bytes32) (runs: 256, μ: 1723133, ~: 1325776) +DefaultFallbackRoutingIsmTest:testConstructorReverts() (gas: 39561) +DefaultFallbackRoutingIsmTest:testRemove(uint32) (runs: 264, μ: 414644, ~: 414516) +DefaultFallbackRoutingIsmTest:testRoute(uint32,bytes32) (runs: 264, μ: 500405, ~: 501461) +DefaultFallbackRoutingIsmTest:testSet(uint32) (runs: 264, μ: 479026, ~: 480157) +DefaultFallbackRoutingIsmTest:testSetManyViaFactory(uint8,uint32) (runs: 263, μ: 43014488, ~: 27800901) +DefaultFallbackRoutingIsmTest:testSetNonOwner(uint32,address) (runs: 264, μ: 11319, ~: 11319) +DefaultFallbackRoutingIsmTest:testVerify(uint32,bytes32) (runs: 264, μ: 509956, ~: 511012) +DefaultFallbackRoutingIsmTest:testVerifyNoIsm(uint32,bytes32) (runs: 264, μ: 530932, ~: 530932) +DomainRoutingHookTest:testHookType() (gas: 5442) +DomainRoutingHookTest:test_postDispatch(uint32,bytes32,bytes,bytes) (runs: 264, μ: 78385, ~: 78332) +DomainRoutingHookTest:test_postDispatch_whenDestinationUnenrolled(uint32,bytes32,bytes,bytes) (runs: 264, μ: 24757, ~: 24606) +DomainRoutingHookTest:test_quoteDispatch(uint32,bytes32,bytes,bytes,uint256) (runs: 264, μ: 77686, ~: 78852) +DomainRoutingHookTest:test_quoteDispatch_whenDestinationUnenrolled(uint32,bytes32,bytes,bytes,uint256) (runs: 264, μ: 24780, ~: 24665) +DomainRoutingIsmTest:testRemove(uint32) (runs: 264, μ: 414603, ~: 414471) +DomainRoutingIsmTest:testRoute(uint32,bytes32) (runs: 264, μ: 501454, ~: 502439) +DomainRoutingIsmTest:testSet(uint32) (runs: 264, μ: 479936, ~: 481090) +DomainRoutingIsmTest:testSetManyViaFactory(uint8,uint32) (runs: 263, μ: 45052230, ~: 23704905) +DomainRoutingIsmTest:testSetNonOwner(uint32,address) (runs: 264, μ: 11252, ~: 11252) +DomainRoutingIsmTest:testVerify(uint32,bytes32) (runs: 264, μ: 512155, ~: 513057) +DomainRoutingIsmTest:testVerifyNoIsm(uint32,bytes32) (runs: 264, μ: 515319, ~: 515092) +ERC5164IsmTest:testTypes() (gas: 10833) +ERC5164IsmTest:test_constructor() (gas: 276193) +ERC5164IsmTest:test_postDispatch() (gas: 67140) +ERC5164IsmTest:test_postDispatch_RevertWhen_ChainIDNotSupported() (gas: 67978) +ERC5164IsmTest:test_postDispatch_RevertWhen_msgValueNotAllowed() (gas: 58207) +ERC5164IsmTest:test_verify() (gas: 49951) +ERC5164IsmTest:test_verifyMessageId() (gas: 37576) +ERC5164IsmTest:test_verifyMessageId_RevertWhen_NotAuthorized() (gas: 13332) +ERC5164IsmTest:test_verify_RevertWhen_InvalidMessage() (gas: 49142) +FallbackDomainRoutingHookTest:testHookType() (gas: 5465) +FallbackDomainRoutingHookTest:test_postDispatch(uint32,bytes32,bytes,bytes) (runs: 264, μ: 77047, ~: 76769) +FallbackDomainRoutingHookTest:test_postDispatch_whenDestinationUnenrolled(uint32,bytes32,bytes,bytes) (runs: 264, μ: 53652, ~: 53374) +FallbackDomainRoutingHookTest:test_quoteDispatch(uint32,bytes32,bytes,bytes,uint256) (runs: 264, μ: 76395, ~: 77404) +FallbackDomainRoutingHookTest:test_quoteDispatch_whenDestinationUnenrolled(uint32,bytes32,bytes,bytes,uint256) (runs: 264, μ: 53005, ~: 54014) +GasRouterTest:testDispatch(uint256) (runs: 263, μ: 451401, ~: 451401) +GasRouterTest:testQuoteGasPayment(uint256) (runs: 262, μ: 126480, ~: 126480) +GasRouterTest:testSetDestinationGas(uint256) (runs: 264, μ: 62359, ~: 64470) +HypERC20CollateralTest:testBenchmark_overheadGasUsage() (gas: 56236) +HypERC20CollateralTest:testInitialize_revert_ifAlreadyInitialized() (gas: 187) +HypERC20CollateralTest:testRemoteTransfer() (gas: 202605) +HypERC20CollateralTest:testRemoteTransfer_invalidAllowance() (gas: 79760) +HypERC20CollateralTest:testRemoteTransfer_withCustomGasConfig() (gas: 276155) +HypERC20CollateralTest:testTransfer_withHookSpecified() (gas: 187232) +HypERC20CollateralVaultDepositTest:testBenchmark_overheadGasUsage() (gas: 306868) +HypERC20CollateralVaultDepositTest:testERC4626VaultDeposit_RemoteTransfer_deposits_intoVault(uint256) (runs: 264, μ: 338485, ~: 344840) +HypERC20CollateralVaultDepositTest:testERC4626VaultDeposit_RemoteTransfer_sweep_excessShares12312(uint256) (runs: 264, μ: 403235, ~: 403320) +HypERC20CollateralVaultDepositTest:testERC4626VaultDeposit_RemoteTransfer_sweep_excessSharesMultipleDeposit(uint256) (runs: 264, μ: 550347, ~: 550497) +HypERC20CollateralVaultDepositTest:testERC4626VaultDeposit_RemoteTransfer_sweep_noExcessShares(uint256) (runs: 264, μ: 366470, ~: 372825) +HypERC20CollateralVaultDepositTest:testERC4626VaultDeposit_RemoteTransfer_sweep_revertNonOwner(uint256) (runs: 264, μ: 396450, ~: 396596) +HypERC20CollateralVaultDepositTest:testERC4626VaultDeposit_RemoteTransfer_withdraws_fromVault(uint256) (runs: 264, μ: 425998, ~: 429613) +HypERC20CollateralVaultDepositTest:testERC4626VaultDeposit_RemoteTransfer_withdraws_lessShares(uint256) (runs: 264, μ: 399801, ~: 399945) +HypERC20CollateralVaultDepositTest:testTransfer_withHookSpecified() (gas: 296040) +HypERC20Test:testBenchmark_overheadGasUsage() (gas: 58924) +HypERC20Test:testDecimals() (gas: 12616) +HypERC20Test:testInitialize_revert_ifAlreadyInitialized() (gas: 24085) +HypERC20Test:testLocalTransfers() (gas: 51917) +HypERC20Test:testRemoteTransfer() (gas: 201085) +HypERC20Test:testRemoteTransfer_invalidAmount() (gas: 88689) +HypERC20Test:testRemoteTransfer_withCustomGasConfig() (gas: 254519) +HypERC20Test:testTotalSupply() (gas: 14717) +HypERC20Test:testTransfer_withHookSpecified() (gas: 209709) +HypERC721CollateralTest:testBenchmark_overheadGasUsage() (gas: 92150) +HypERC721CollateralTest:testInitialize_revert_ifAlreadyInitialized() (gas: 165) +HypERC721CollateralTest:testRemoteTransfer(bool) (runs: 264, μ: 4076396, ~: 4849034) +HypERC721CollateralTest:testRemoteTransfer_revert_invalidTokenId() (gas: 4708073) +HypERC721CollateralTest:testRemoteTransfer_revert_unowned() (gas: 4779020) +HypERC721CollateralURIStorageTest:testBenchmark_overheadGasUsage() (gas: 85000) +HypERC721CollateralURIStorageTest:testRemoteTransfers_revert_burned() (gas: 4848986) +HypERC721Test:testBenchmark_overheadGasUsage() (gas: 151235) +HypERC721Test:testInitialize_revert_ifAlreadyInitialized() (gas: 20042) +HypERC721Test:testLocalTransfer() (gas: 82548) +HypERC721Test:testLocalYTransfer_revert_invalidTokenId() (gas: 17998) +HypERC721Test:testOwnerOf() (gas: 14951) +HypERC721Test:testRemoteTransfer(bool) (runs: 264, μ: 4061653, ~: 4834291) +HypERC721Test:testRemoteTransfer_revert_invalidTokenId() (gas: 4701257) +HypERC721Test:testRemoteTransfer_revert_unowned() (gas: 4764152) +HypERC721Test:testTotalSupply() (gas: 15024) +HypERC721URIStorageTest:testBenchmark_overheadGasUsage() (gas: 148168) +HypERC721URIStorageTest:testRemoteTransfers_revert_burned() (gas: 4832405) +HypNativeScaledTest:test_constructor() (gas: 14718) +HypNativeScaledTest:test_handle(uint256) (runs: 259, μ: 398794, ~: 403823) +HypNativeScaledTest:test_handle_reverts_whenAmountExceedsSupply(uint256) (runs: 259, μ: 367852, ~: 369833) +HypNativeScaledTest:test_receive(uint256) (runs: 261, μ: 24653, ~: 25167) +HypNativeScaledTest:test_tranferRemote(uint256) (runs: 259, μ: 397622, ~: 402381) +HypNativeScaledTest:test_transferRemote_reverts_whenAmountExceedsValue(uint256) (runs: 261, μ: 24533, ~: 25047) +HypNativeTest:testBenchmark_overheadGasUsage() (gas: 54702) +HypNativeTest:testInitialize_revert_ifAlreadyInitialized() (gas: 165) +HypNativeTest:testRemoteTransfer() (gas: 178831) +HypNativeTest:testRemoteTransfer_invalidAmount() (gas: 74686) +HypNativeTest:testRemoteTransfer_withCustomGasConfig() (gas: 245800) +HypNativeTest:testTransfer_withHookSpecified() (gas: 190418) +InterchainAccountRouterTest:testFuzz_callRemoteWithDefault(bytes32) (runs: 264, μ: 808391, ~: 808618) +InterchainAccountRouterTest:testFuzz_callRemoteWithFailingDefaultIsm(bytes32) (runs: 264, μ: 792018, ~: 792018) +InterchainAccountRouterTest:testFuzz_callRemoteWithFailingIsmOverride(bytes32) (runs: 264, μ: 801017, ~: 801017) +InterchainAccountRouterTest:testFuzz_callRemoteWithOverrides_default(bytes32) (runs: 264, μ: 690802, ~: 690878) +InterchainAccountRouterTest:testFuzz_callRemoteWithOverrides_metadata(uint64,bytes32) (runs: 264, μ: 691093, ~: 692158) +InterchainAccountRouterTest:testFuzz_callRemoteWithoutDefaults_revert_noRouter(bytes32) (runs: 264, μ: 30012, ~: 30012) +InterchainAccountRouterTest:testFuzz_constructor(address) (runs: 264, μ: 121698, ~: 121698) +InterchainAccountRouterTest:testFuzz_customMetadata_forIgp(uint64,uint64,bytes32) (runs: 264, μ: 816718, ~: 817142) +InterchainAccountRouterTest:testFuzz_customMetadata_reverts_underpayment(uint64,uint64,bytes32) (runs: 261, μ: 276356, ~: 278128) +InterchainAccountRouterTest:testFuzz_enrollRemoteRouterAndIsm(bytes32,bytes32) (runs: 264, μ: 141323, ~: 141323) +InterchainAccountRouterTest:testFuzz_enrollRemoteRouterAndIsmImmutable(bytes32,bytes32,bytes32,bytes32) (runs: 264, μ: 136643, ~: 136643) +InterchainAccountRouterTest:testFuzz_enrollRemoteRouterAndIsmNonOwner(address,bytes32,bytes32) (runs: 264, μ: 28617, ~: 28617) +InterchainAccountRouterTest:testFuzz_enrollRemoteRouterAndIsms(uint32[],bytes32[],bytes32[]) (runs: 264, μ: 44699, ~: 45978) +InterchainAccountRouterTest:testFuzz_enrollRemoteRouters(uint8,uint32,bytes32) (runs: 262, μ: 9965893, ~: 9477545) +InterchainAccountRouterTest:testFuzz_getDeployedInterchainAccount_checkAccountOwners(address) (runs: 264, μ: 125334, ~: 125485) +InterchainAccountRouterTest:testFuzz_getLocalInterchainAccount(bytes32) (runs: 264, μ: 697506, ~: 697582) +InterchainAccountRouterTest:testFuzz_getRemoteInterchainAccount(address,address) (runs: 264, μ: 141026, ~: 141102) +InterchainAccountRouterTest:testFuzz_overrideAndCallRemote(bytes32) (runs: 264, μ: 808413, ~: 808640) +InterchainAccountRouterTest:testFuzz_receiveValue(uint256) (runs: 261, μ: 165407, ~: 165407) +InterchainAccountRouterTest:testFuzz_sendValue(uint256) (runs: 261, μ: 711950, ~: 711950) +InterchainAccountRouterTest:testFuzz_singleCallRemoteWithDefault(bytes32) (runs: 264, μ: 809119, ~: 809346) +InterchainAccountRouterTest:test_quoteGasPayment() (gas: 167210) +InterchainAccountRouterTest:test_quoteGasPayment_gasLimitOverride() (gas: 167759) +InterchainGasPaymasterTest:testClaim() (gas: 96594) +InterchainGasPaymasterTest:testConstructorSetsBeneficiary() (gas: 7682) +InterchainGasPaymasterTest:testConstructorSetsDeployedBlock() (gas: 7597) +InterchainGasPaymasterTest:testGetExchangeRateAndGasPrice() (gas: 42913) +InterchainGasPaymasterTest:testGetExchangeRateAndGasPriceRevertsIfNoGasOracleSet() (gas: 12259) +InterchainGasPaymasterTest:testHookType() (gas: 5487) +InterchainGasPaymasterTest:testInitializeRevertsIfCalledTwice() (gas: 11036) +InterchainGasPaymasterTest:testPayForGas() (gas: 95965) +InterchainGasPaymasterTest:testPayForGas_reverts_ifPaymentInsufficient() (gas: 46057) +InterchainGasPaymasterTest:testPayForGas_withOverhead(uint128,uint96) (runs: 264, μ: 71381, ~: 71381) +InterchainGasPaymasterTest:testPostDispatch__withOverheadSet(uint96) (runs: 264, μ: 80957, ~: 80957) +InterchainGasPaymasterTest:testPostDispatch_customWithMetadata() (gas: 113183) +InterchainGasPaymasterTest:testPostDispatch_customWithMetadataAndOverhead(uint96) (runs: 264, μ: 80138, ~: 80138) +InterchainGasPaymasterTest:testPostDispatch_defaultGasLimit() (gas: 77124) +InterchainGasPaymasterTest:testQuoteDispatch_customWithMetadata() (gas: 55203) +InterchainGasPaymasterTest:testQuoteDispatch_defaultGasLimit() (gas: 53566) +InterchainGasPaymasterTest:testQuoteGasPaymentRemoteVeryCheap() (gas: 42817) +InterchainGasPaymasterTest:testQuoteGasPaymentRemoteVeryExpensive() (gas: 42901) +InterchainGasPaymasterTest:testQuoteGasPaymentRevertsIfNoGasOracleSet() (gas: 12182) +InterchainGasPaymasterTest:testQuoteGasPaymentSimilarExchangeRate() (gas: 42945) +InterchainGasPaymasterTest:testSetBeneficiary() (gas: 18766) +InterchainGasPaymasterTest:testSetBeneficiaryRevertsIfNotOwner() (gas: 11015) +InterchainGasPaymasterTest:testSetDestinationGasConfigs(uint32,uint32,uint96,uint96) (runs: 264, μ: 862110, ~: 862110) +InterchainGasPaymasterTest:testSetDestinationGasConfigs_reverts_notOwner(uint32,uint32,uint96,uint96) (runs: 264, μ: 17237, ~: 17237) +InterchainGasPaymasterTest:testdestinationGasLimit(uint96) (runs: 264, μ: 19766, ~: 19766) +InterchainGasPaymasterTest:testdestinationGasLimit_whenOverheadNotSet(uint32) (runs: 264, μ: 10959, ~: 10959) +InterchainQueryRouterTest:testCannotCallbackReverting() (gas: 1547616) +InterchainQueryRouterTest:testCannotQueryReverting() (gas: 1120274) +InterchainQueryRouterTest:testQueryAddress(address) (runs: 264, μ: 1558785, ~: 1558785) +InterchainQueryRouterTest:testQueryUint256(uint256) (runs: 264, μ: 1832946, ~: 1833248) +InterchainQueryRouterTest:testSingleQueryAddress(address) (runs: 264, μ: 1558760, ~: 1558760) +LayerZeroV1HookTest:testLzV1Hook_HookType() (gas: 5441) +LayerZeroV1HookTest:testLzV1Hook_ParseLzMetadata_returnsCorrectData() (gas: 34463) +LayerZeroV1HookTest:testLzV1Hook_PostDispatch_executesCrossChain() (gas: 270186) +LayerZeroV1HookTest:testLzV1Hook_PostDispatch_notEnoughFee() (gas: 183777) +LayerZeroV1HookTest:testLzV1Hook_PostDispatch_refundExtraFee() (gas: 303417) +LayerZeroV1HookTest:testLzV1Hook_QuoteDispatch_returnsFeeAmount() (gas: 58280) +LayerZeroV2HookTest:testLzV2Hook_HookType() (gas: 5530) +LayerZeroV2HookTest:testLzV2Hook_ParseLzMetadata_returnsCorrectData() (gas: 16589) +LayerZeroV2HookTest:testLzV2Hook_PostDispatch_executesCrossChain() (gas: 210581) +LayerZeroV2HookTest:testLzV2Hook_PostDispatch_notEnoughFee(uint256) (runs: 259, μ: 202092, ~: 207603) +LayerZeroV2HookTest:testLzV2Hook_PostDispatch_refundExtraFee(uint256) (runs: 261, μ: 248680, ~: 248680) +LayerZeroV2HookTest:testLzV2Hook_QuoteDispatch_returnsFeeAmount() (gas: 47969) +LayerZeroV2IsmTest:testLzV2Ism_lzReceive_RevertWhen_NotCalledByEndpoint(address) (runs: 264, μ: 92135, ~: 92135) +LayerZeroV2IsmTest:testLzV2Ism_lzReceive_RevertWhen_NotSentByHook(address) (runs: 264, μ: 91885, ~: 91885) +LayerZeroV2IsmTest:testLzV2Ism_verifyMessageId_SetsCorrectMessageId(bytes32) (runs: 264, μ: 89258, ~: 89258) +LibBitTest:testClearBit(uint8) (runs: 264, μ: 25093, ~: 25109) +LibBitTest:testIsBitSet(uint8) (runs: 264, μ: 22781, ~: 22796) +LibBitTest:testSetBit(uint8) (runs: 264, μ: 22698, ~: 22713) +LiquidityLayerRouterTest:testCannotSendToRecipientWithoutHandle() (gas: 739136) +LiquidityLayerRouterTest:testDispatchWithTokenTransfersMovesTokens() (gas: 581615) +LiquidityLayerRouterTest:testDispatchWithTokensCallsAdapter() (gas: 587715) +LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithFailedTransferIn() (gas: 29553) +LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithUnkownBridgeAdapter() (gas: 20642) +LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 838861) +LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 671688) +LiquidityLayerRouterTest:testSendToRecipientWithoutHandleWhenSpecifyingNoMessage() (gas: 1297563) +LiquidityLayerRouterTest:testSetLiquidityLayerAdapter() (gas: 23432) +MailboxTest:test_100dispatch_withMerkleTreeHook(bytes) (runs: 264, μ: 4364419, ~: 4347734) +MailboxTest:test_dispatch(uint8,bytes,bytes) (runs: 264, μ: 9255880, ~: 7157027) +MailboxTest:test_initialize() (gas: 25344) +MailboxTest:test_initialize_revertsWhenCalledTwice() (gas: 19719) +MailboxTest:test_localDomain() (gas: 5714) +MailboxTest:test_process(bytes,bytes,uint256) (runs: 262, μ: 168477, ~: 160475) +MailboxTest:test_process_revertsWhenAlreadyDelivered() (gas: 109577) +MailboxTest:test_process_revertsWhenBadDestination(bytes) (runs: 264, μ: 13155, ~: 13131) +MailboxTest:test_process_revertsWhenBadVersion(bytes) (runs: 264, μ: 12959, ~: 12935) +MailboxTest:test_process_revertsWhenISMFails(bytes) (runs: 264, μ: 64059, ~: 63986) +MailboxTest:test_quoteDispatch(uint256,uint256,uint256,bytes,bytes) (runs: 258, μ: 137539, ~: 138983) +MailboxTest:test_recipientIsm() (gas: 296559) +MailboxTest:test_setDefaultHook() (gas: 233128) +MailboxTest:test_setDefaultIsm() (gas: 180212) +MailboxTest:test_setRequiredHook() (gas: 233041) +MerkleRootMultisigIsmTest:testFailVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 258, μ: 325435, ~: 319198) +MerkleRootMultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 258, μ: 328022, ~: 320212) +MerkleTreeHookTest:testHookType() (gas: 5535) +MerkleTreeHookTest:testPostDispatch_emit() (gas: 278226) +MerkleTreeHookTest:testQuoteDispatch() (gas: 46972) +MessageIdMultisigIsmTest:testFailVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 258, μ: 305244, ~: 299975) +MessageIdMultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 258, μ: 306844, ~: 299697) +MessagingTest:testSendMessage(string) (runs: 264, μ: 356834, ~: 336547) +OPStackIsmTest:testFork_postDispatch() (gas: 3873300) +OPStackIsmTest:testFork_postDispatch_RevertWhen_ChainIDNotSupported() (gas: 3751565) +OPStackIsmTest:testFork_postDispatch_RevertWhen_NotLastDispatchedMessage() (gas: 3730913) +OPStackIsmTest:testFork_postDispatch_RevertWhen_TooMuchValue() (gas: 3746869) +OPStackIsmTest:testFork_quoteDispatch() (gas: 3727553) +OPStackIsmTest:testFork_verify() (gas: 3788803) +OPStackIsmTest:testFork_verifyMessageId() (gas: 3780276) +OPStackIsmTest:testFork_verifyMessageId_RevertWhen_NotAuthorized() (gas: 3719317) +OPStackIsmTest:testFork_verify_RevertWhen_HyperlaneInvalidMessage() (gas: 3787506) +OPStackIsmTest:testFork_verify_RevertWhen_InvalidOptimismMessageID() (gas: 3796582) +OPStackIsmTest:testFork_verify_WithValue(uint256) (runs: 18, μ: 3840947, ~: 3843413) +OPStackIsmTest:testFork_verify_tooMuchValue() (gas: 3787164) +OPStackIsmTest:testFork_verify_valueAlreadyClaimed(uint256) (runs: 18, μ: 3845394, ~: 3847860) +PausableIsmTest:test_pause() (gas: 21155) +PausableIsmTest:test_unpause() (gas: 19011) +PausableIsmTest:test_verify() (gas: 20458) +PortalAdapterTest:testAdapter(uint256) (runs: 264, μ: 147871, ~: 147992) +PortalAdapterTest:testReceivingRevertsWithoutTransferCompletion(uint256) (runs: 264, μ: 152782, ~: 152903) +PortalAdapterTest:testReceivingWorks(uint256) (runs: 264, μ: 240060, ~: 240178) +ProtocolFeeTest:testConstructor() (gas: 7608) +ProtocolFeeTest:testFuzz_collectProtocolFee(uint256,uint256) (runs: 264, μ: 4543919, ~: 4512170) +ProtocolFeeTest:testFuzz_postDispatch_inusfficientFees(uint256,uint256) (runs: 264, μ: 33081, ~: 33968) +ProtocolFeeTest:testFuzz_postDispatch_sufficientFees(uint256,uint256) (runs: 264, μ: 76503, ~: 78212) +ProtocolFeeTest:testHookType() (gas: 5487) +ProtocolFeeTest:testQuoteDispatch() (gas: 17254) +ProtocolFeeTest:testSetBeneficiary_revertWhen_notOwner() (gas: 18169) +ProtocolFeeTest:testSetProtocolFee(uint256) (runs: 264, μ: 18110, ~: 18211) +ProtocolFeeTest:testSetProtocolFee_revertWhen_exceedsMax(uint256) (runs: 264, μ: 19652, ~: 19749) +ProtocolFeeTest:testSetProtocolFee_revertsWhen_notOwner() (gas: 15900) +ProtocolFeeTest:test_postDispatch_specifyRefundAddress(uint256,uint256) (runs: 264, μ: 78195, ~: 79779) +RateLimitLibTest:testRateLimited_decreasesLimitWithinSameDay() (gas: 47046) +RateLimitLibTest:testRateLimited_neverReturnsGtMaxLimit(uint256,uint40) (runs: 260, μ: 41965, ~: 43595) +RateLimitLibTest:testRateLimited_onlyOwnerCanSetTargetLimit() (gas: 10842) +RateLimitLibTest:testRateLimited_replinishesWithinSameDay() (gas: 44005) +RateLimitLibTest:testRateLimited_returnsCurrentFilledLevel_anyDay(uint40) (runs: 264, μ: 19500, ~: 20149) +RateLimitLibTest:testRateLimited_revertsIfMaxNotSet() (gas: 13082) +RateLimitLibTest:testRateLimited_setsNewLimit() (gas: 16273) +RateLimitLibTest:testRateLimited_shouldResetLimit_ifDurationExceeds(uint256) (runs: 260, μ: 43249, ~: 43249) +RateLimitedHookTest:testRateLimitedHook_allowsTransfer_ifUnderLimit(uint128,uint128) (runs: 261, μ: 219047, ~: 220731) +RateLimitedHookTest:testRateLimitedHook_preventsDuplicateMessageFromValidating(uint128) (runs: 261, μ: 226900, ~: 228379) +RateLimitedHookTest:testRateLimitedHook_revertsIfCalledByNonMailbox(bytes) (runs: 264, μ: 17411, ~: 17393) +RateLimitedHookTest:testRateLimitedHook_revertsTransfer_ifExceedsFilledLevel(uint128,uint128) (runs: 259, μ: 192624, ~: 191383) +RateLimitedIsmTest:testRateLimitedIsm_preventsDuplicateMessageFromValidating(uint128) (runs: 261, μ: 192457, ~: 193601) +RateLimitedIsmTest:testRateLimitedIsm_revertsIDeliveredFalse(bytes) (runs: 264, μ: 37879, ~: 37861) +RateLimitedIsmTest:testRateLimitedIsm_verify(uint128) (runs: 261, μ: 189425, ~: 190569) +RouterTest:testDispatch(bytes32) (runs: 264, μ: 280253, ~: 280253) +RouterTest:testDispatchInsufficientPayment(bytes32) (runs: 264, μ: 228382, ~: 228382) +RouterTest:testEnrolledMailboxAndRouter(bytes32) (runs: 264, μ: 109663, ~: 109663) +RouterTest:testInitialize() (gas: 27716) +RouterTest:testNotOwnerEnrollRouter(address,bytes32) (runs: 263, μ: 14929, ~: 14929) +RouterTest:testOwnerBatchEnrollRouter(bytes32) (runs: 264, μ: 113515, ~: 113515) +RouterTest:testOwnerBatchUnenrollRouter(bytes32) (runs: 264, μ: 88218, ~: 88192) +RouterTest:testOwnerEnrollRouter(bytes32) (runs: 264, μ: 111875, ~: 111875) +RouterTest:testOwnerUnenrollRouter(bytes32) (runs: 264, μ: 87295, ~: 87269) +RouterTest:testReturnDomains(bytes32) (runs: 264, μ: 176310, ~: 176310) +RouterTest:testUnenrolledMailbox(bytes32) (runs: 264, μ: 13782, ~: 13782) +RouterTest:testUnenrolledRouter(bytes32) (runs: 264, μ: 25300, ~: 25300) +StorageGasOracleTest:testConstructorSetsOwnership() (gas: 7537) +StorageGasOracleTest:testGetExchangeRateAndGasPrice() (gas: 12412) StorageGasOracleTest:testGetExchangeRateAndGasPriceUnknownDomain() (gas: 8064) StorageGasOracleTest:testSetRemoteGasData() (gas: 38836) -StorageGasOracleTest:testSetRemoteGasDataConfigs() (gas: 69238) -StorageGasOracleTest:testSetRemoteGasDataConfigsRevertsIfNotOwner() (gas: 12227) -StorageGasOracleTest:testSetRemoteGasDataRevertsIfNotOwner() (gas: 11275) -TestQuerySenderTest:testSendAddressQuery(address) (runs: 256, μ: 957141, ~: 957608) -TestQuerySenderTest:testSendAddressQueryRequiresGasPayment() (gas: 329870) -TestQuerySenderTest:testSendBytesQuery(uint256) (runs: 256, μ: 1590463, ~: 1591085) -TestQuerySenderTest:testSendBytesQueryRequiresGasPayment() (gas: 329891) -TestQuerySenderTest:testSendUint256Query(uint256) (runs: 256, μ: 1590538, ~: 1591160) -TestQuerySenderTest:testSendUint256QueryRequiresGasPayment() (gas: 329858) -ValidatorAnnounceTest:testAnnounce() (gas: 245554) \ No newline at end of file +StorageGasOracleTest:testSetRemoteGasDataConfigs() (gas: 69080) +StorageGasOracleTest:testSetRemoteGasDataConfigsRevertsIfNotOwner() (gas: 12250) +StorageGasOracleTest:testSetRemoteGasDataRevertsIfNotOwner() (gas: 11253) +TestQuerySenderTest:testSendAddressQuery(address) (runs: 264, μ: 1200059, ~: 1200135) +TestQuerySenderTest:testSendBytesQuery(uint256) (runs: 264, μ: 1834102, ~: 1834253) +TestQuerySenderTest:testSendUint256Query(uint256) (runs: 264, μ: 1834177, ~: 1834328) +TestSendReceiverTest:testDispatchToSelf() (gas: 168436) +TestSendReceiverTest:testDispatchToSelf_withHook() (gas: 168925) +TestSendReceiverTest:testHandle(uint256) (runs: 264, μ: 11941, ~: 12137) +TrustedRelayerIsmTest:test_verify(uint32,bytes32,bytes) (runs: 264, μ: 185352, ~: 177289) +ValidatorAnnounceTest:testAnnounce() (gas: 356026) \ No newline at end of file diff --git a/solidity/CHANGELOG.md b/solidity/CHANGELOG.md index eb26ccd27e..bb3548828d 100644 --- a/solidity/CHANGELOG.md +++ b/solidity/CHANGELOG.md @@ -1,5 +1,25 @@ # @hyperlane-xyz/core +## 3.11.1 + +### Patch Changes + +- @hyperlane-xyz/utils@3.11.1 + +## 3.11.0 + +### Minor Changes + +- b6fdf2f7f: Implement XERC20 and FiatToken collateral warp routes +- b63714ede: Convert all public hyperlane npm packages from CJS to pure ESM + +### Patch Changes + +- Updated dependencies [b63714ede] +- Updated dependencies [2b3f75836] +- Updated dependencies [af2634207] + - @hyperlane-xyz/utils@3.11.0 + ## 3.10.0 ### Minor Changes diff --git a/solidity/README.md b/solidity/README.md index f6af38b8ef..d800b4a1c4 100644 --- a/solidity/README.md +++ b/solidity/README.md @@ -6,14 +6,30 @@ Hyperlane Core contains the contracts and typechain artifacts for the Hyperlane ```bash # Install with NPM -npm install @hyperlane-xyz/utils +npm install @hyperlane-xyz/core # Or with Yarn -yarn add @hyperlane-xyz/utils +yarn add @hyperlane-xyz/core ``` Note, this package uses [ESM Modules](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#pure-esm-package) +## Build + +```bash +yarn build +``` + +## Test + +```bash +yarn test +``` + +### Fixtures + +Some forge tests may generate fixtures in the [fixtures](./fixtures/) directory. This allows [SDK](../typescript/sdk) tests to leverage forge fuzzing. These are git ignored and should not be committed. + ## License Apache 2.0 diff --git a/solidity/contracts/avs/ECDSAServiceManagerBase.sol b/solidity/contracts/avs/ECDSAServiceManagerBase.sol new file mode 100644 index 0000000000..7fdb67fbf4 --- /dev/null +++ b/solidity/contracts/avs/ECDSAServiceManagerBase.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.0; + +import {ISignatureUtils} from "../interfaces/avs/vendored/ISignatureUtils.sol"; +import {IAVSDirectory} from "../interfaces/avs/vendored/IAVSDirectory.sol"; + +import {IServiceManager} from "../interfaces/avs/vendored/IServiceManager.sol"; +import {IServiceManagerUI} from "../interfaces/avs/vendored/IServiceManagerUI.sol"; +import {IDelegationManager} from "../interfaces/avs/vendored/IDelegationManager.sol"; +import {IStrategy} from "../interfaces/avs/vendored/IStrategy.sol"; +import {IPaymentCoordinator} from "../interfaces/avs/vendored/IPaymentCoordinator.sol"; +import {Quorum} from "../interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol"; +import {ECDSAStakeRegistry} from "./ECDSAStakeRegistry.sol"; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/// @author Layr Labs, Inc. +abstract contract ECDSAServiceManagerBase is + IServiceManager, + OwnableUpgradeable +{ + /// @notice Address of the stake registry contract, which manages registration and stake recording. + address public immutable stakeRegistry; + + /// @notice Address of the AVS directory contract, which manages AVS-related data for registered operators. + address public immutable avsDirectory; + + /// @notice Address of the delegation manager contract, which manages staker delegations to operators. + address internal immutable delegationManager; + + // ============ Public Storage ============ + + /// @notice Address of the payment coordinator contract, which handles payment distributions. Will be set once live on Eigenlayer. + address internal paymentCoordinator; + + // ============ Modifiers ============ + + /** + * @dev Ensures that the function is only callable by the `stakeRegistry` contract. + * This is used to restrict certain registration and deregistration functionality to the `stakeRegistry` + */ + modifier onlyStakeRegistry() { + require( + msg.sender == stakeRegistry, + "ECDSAServiceManagerBase.onlyStakeRegistry: caller is not the stakeRegistry" + ); + _; + } + + // ============ Events ============ + + /** + * @notice Emitted when an operator is registered to the AVS + * @param operator The address of the operator + */ + event OperatorRegisteredToAVS(address indexed operator); + + /** + * @notice Emitted when an operator is deregistered from the AVS + * @param operator The address of the operator + */ + event OperatorDeregisteredFromAVS(address indexed operator); + + // ============ Constructor ============ + + /** + * @dev Constructor for ECDSAServiceManagerBase, initializing immutable contract addresses and disabling initializers. + * @param _avsDirectory The address of the AVS directory contract, managing AVS-related data for registered operators. + * @param _stakeRegistry The address of the stake registry contract, managing registration and stake recording. + * @param _paymentCoordinator The address of the payment coordinator contract, handling payment distributions. + * @param _delegationManager The address of the delegation manager contract, managing staker delegations to operators. + */ + constructor( + address _avsDirectory, + address _stakeRegistry, + address _paymentCoordinator, + address _delegationManager + ) { + avsDirectory = _avsDirectory; + stakeRegistry = _stakeRegistry; + paymentCoordinator = _paymentCoordinator; + delegationManager = _delegationManager; + } + + /** + * @dev Initializes the base service manager by transferring ownership to the initial owner. + * @param initialOwner The address to which the ownership of the contract will be transferred. + */ + function __ServiceManagerBase_init( + address initialOwner + ) internal virtual onlyInitializing { + _transferOwnership(initialOwner); + } + + /// @inheritdoc IServiceManagerUI + function updateAVSMetadataURI( + string memory _metadataURI + ) external virtual onlyOwner { + _updateAVSMetadataURI(_metadataURI); + } + + /// @inheritdoc IServiceManager + function payForRange( + IPaymentCoordinator.RangePayment[] calldata rangePayments + ) external virtual onlyOwner { + _payForRange(rangePayments); + } + + /// @inheritdoc IServiceManagerUI + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external virtual onlyStakeRegistry { + _registerOperatorToAVS(operator, operatorSignature); + } + + /// @inheritdoc IServiceManagerUI + function deregisterOperatorFromAVS( + address operator + ) external virtual onlyStakeRegistry { + _deregisterOperatorFromAVS(operator); + } + + /// @inheritdoc IServiceManagerUI + function getRestakeableStrategies() + external + view + virtual + returns (address[] memory) + { + return _getRestakeableStrategies(); + } + + /// @inheritdoc IServiceManagerUI + function getOperatorRestakedStrategies( + address _operator + ) external view virtual returns (address[] memory) { + return _getOperatorRestakedStrategies(_operator); + } + + /** + * @notice Sets the address of the payment coordinator contract. + * @dev This function is only callable by the contract owner. + * @param _paymentCoordinator The address of the payment coordinator contract. + */ + function setPaymentCoordinator( + address _paymentCoordinator + ) external virtual onlyOwner { + paymentCoordinator = _paymentCoordinator; + } + + /** + * @notice Forwards the call to update AVS metadata URI in the AVSDirectory contract. + * @dev This internal function is a proxy to the `updateAVSMetadataURI` function of the AVSDirectory contract. + * @param _metadataURI The new metadata URI to be set. + */ + function _updateAVSMetadataURI( + string memory _metadataURI + ) internal virtual { + IAVSDirectory(avsDirectory).updateAVSMetadataURI(_metadataURI); + } + + /** + * @notice Forwards the call to register an operator in the AVSDirectory contract. + * @dev This internal function is a proxy to the `registerOperatorToAVS` function of the AVSDirectory contract. + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry details of the operator's registration. + */ + function _registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) internal virtual { + IAVSDirectory(avsDirectory).registerOperatorToAVS( + operator, + operatorSignature + ); + emit OperatorRegisteredToAVS(operator); + } + + /** + * @notice Forwards the call to deregister an operator from the AVSDirectory contract. + * @dev This internal function is a proxy to the `deregisterOperatorFromAVS` function of the AVSDirectory contract. + * @param operator The address of the operator to deregister. + */ + function _deregisterOperatorFromAVS(address operator) internal virtual { + IAVSDirectory(avsDirectory).deregisterOperatorFromAVS(operator); + emit OperatorDeregisteredFromAVS(operator); + } + + /** + * @notice Processes a batch of range payments by transferring the specified amounts from the sender to this contract and then approving the PaymentCoordinator to use these amounts. + * @dev This function handles the transfer and approval of tokens necessary for range payments. It then delegates the actual payment logic to the PaymentCoordinator contract. + * @param rangePayments An array of `RangePayment` structs, each representing a payment for a specific range. + */ + function _payForRange( + IPaymentCoordinator.RangePayment[] calldata rangePayments + ) internal virtual { + for (uint256 i = 0; i < rangePayments.length; ++i) { + rangePayments[i].token.transferFrom( + msg.sender, + address(this), + rangePayments[i].amount + ); + rangePayments[i].token.approve( + paymentCoordinator, + rangePayments[i].amount + ); + } + + IPaymentCoordinator(paymentCoordinator).payForRange(rangePayments); + } + + /** + * @notice Retrieves the addresses of all strategies that are part of the current quorum. + * @dev Fetches the quorum configuration from the ECDSAStakeRegistry and extracts the strategy addresses. + * @return strategies An array of addresses representing the strategies in the current quorum. + */ + function _getRestakeableStrategies() + internal + view + virtual + returns (address[] memory) + { + Quorum memory quorum = ECDSAStakeRegistry(stakeRegistry).quorum(); + address[] memory strategies = new address[](quorum.strategies.length); + for (uint256 i = 0; i < quorum.strategies.length; i++) { + strategies[i] = address(quorum.strategies[i].strategy); + } + return strategies; + } + + /** + * @notice Retrieves the addresses of strategies where the operator has restaked. + * @dev This function fetches the quorum details from the ECDSAStakeRegistry, retrieves the operator's shares for each strategy, + * and filters out strategies with non-zero shares indicating active restaking by the operator. + * @param _operator The address of the operator whose restaked strategies are to be retrieved. + * @return restakedStrategies An array of addresses of strategies where the operator has active restakes. + */ + function _getOperatorRestakedStrategies( + address _operator + ) internal view virtual returns (address[] memory) { + Quorum memory quorum = ECDSAStakeRegistry(stakeRegistry).quorum(); + uint256 count = quorum.strategies.length; + IStrategy[] memory strategies = new IStrategy[](count); + for (uint256 i; i < count; i++) { + strategies[i] = quorum.strategies[i].strategy; + } + uint256[] memory shares = IDelegationManager(delegationManager) + .getOperatorShares(_operator, strategies); + + address[] memory activeStrategies = new address[](count); + uint256 activeCount; + for (uint256 i; i < count; i++) { + if (shares[i] > 0) { + activeCount++; + } + } + + // Resize the array to fit only the active strategies + address[] memory restakedStrategies = new address[](activeCount); + for (uint256 j = 0; j < count; j++) { + if (shares[j] > 0) { + restakedStrategies[j] = activeStrategies[j]; + } + } + + return restakedStrategies; + } + + // storage gap for upgradeability + // slither-disable-next-line shadowing-state + uint256[50] private __GAP; +} diff --git a/solidity/contracts/avs/ECDSAStakeRegistry.sol b/solidity/contracts/avs/ECDSAStakeRegistry.sol new file mode 100644 index 0000000000..0a4e32a011 --- /dev/null +++ b/solidity/contracts/avs/ECDSAStakeRegistry.sol @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import {ECDSAStakeRegistryStorage, Quorum, StrategyParams} from "./ECDSAStakeRegistryStorage.sol"; +import {IStrategy} from "../interfaces/avs/vendored/IStrategy.sol"; +import {IDelegationManager} from "../interfaces/avs/vendored/IDelegationManager.sol"; +import {ISignatureUtils} from "../interfaces/avs/vendored/ISignatureUtils.sol"; +import {IServiceManager} from "../interfaces/avs/vendored/IServiceManager.sol"; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {CheckpointsUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/CheckpointsUpgradeable.sol"; +import {SignatureCheckerUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/SignatureCheckerUpgradeable.sol"; +import {IERC1271Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC1271Upgradeable.sol"; + +/// @title ECDSA Stake Registry +/// @author Layr Labs, Inc. +/// @dev THIS CONTRACT IS NOT AUDITED +/// @notice Manages operator registration and quorum updates for an AVS using ECDSA signatures. +contract ECDSAStakeRegistry is + IERC1271Upgradeable, + OwnableUpgradeable, + ECDSAStakeRegistryStorage +{ + using SignatureCheckerUpgradeable for address; + using CheckpointsUpgradeable for CheckpointsUpgradeable.History; + + /// @dev Constructor to create ECDSAStakeRegistry. + /// @param _delegationManager Address of the DelegationManager contract that this registry interacts with. + constructor( + IDelegationManager _delegationManager + ) ECDSAStakeRegistryStorage(_delegationManager) { + // _disableInitializers(); + } + + /// @notice Initializes the contract with the given parameters. + /// @param _serviceManager The address of the service manager. + /// @param _thresholdWeight The threshold weight in basis points. + /// @param _quorum The quorum struct containing the details of the quorum thresholds. + function initialize( + address _serviceManager, + uint256 _thresholdWeight, + Quorum memory _quorum + ) external initializer { + __ECDSAStakeRegistry_init(_serviceManager, _thresholdWeight, _quorum); + } + + /// @notice Registers a new operator using a provided signature + /// @param _operatorSignature Contains the operator's signature, salt, and expiry + function registerOperatorWithSignature( + address _operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature + ) external { + _registerOperatorWithSig(_operator, _operatorSignature); + } + + /// @notice Deregisters an existing operator + function deregisterOperator() external { + _deregisterOperator(msg.sender); + } + + /** + * @notice Updates the StakeRegistry's view of one or more operators' stakes adding a new entry in their history of stake checkpoints, + * @dev Queries stakes from the Eigenlayer core DelegationManager contract + * @param _operators A list of operator addresses to update + */ + function updateOperators(address[] memory _operators) external { + _updateOperators(_operators); + } + + /** + * @notice Updates the quorum configuration and the set of operators + * @dev Only callable by the contract owner. + * It first updates the quorum configuration and then updates the list of operators. + * @param _quorum The new quorum configuration, including strategies and their new weights + * @param _operators The list of operator addresses to update stakes for + */ + function updateQuorumConfig( + Quorum memory _quorum, + address[] memory _operators + ) external onlyOwner { + _updateQuorumConfig(_quorum); + _updateOperators(_operators); + } + + /// @notice Updates the weight an operator must have to join the operator set + /// @dev Access controlled to the contract owner + /// @param _newMinimumWeight The new weight an operator must have to join the operator set + function updateMinimumWeight( + uint256 _newMinimumWeight, + address[] memory _operators + ) external onlyOwner { + _updateMinimumWeight(_newMinimumWeight); + _updateOperators(_operators); + } + + /** + * @notice Sets a new cumulative threshold weight for message validation by operator set signatures. + * @dev This function can only be invoked by the owner of the contract. It delegates the update to + * an internal function `_updateStakeThreshold`. + * @param _thresholdWeight The updated threshold weight required to validate a message. This is the + * cumulative weight that must be met or exceeded by the sum of the stakes of the signatories for + * a message to be deemed valid. + */ + function updateStakeThreshold(uint256 _thresholdWeight) external onlyOwner { + _updateStakeThreshold(_thresholdWeight); + } + + /// @notice Verifies if the provided signature data is valid for the given data hash. + /// @param _dataHash The hash of the data that was signed. + /// @param _signatureData Encoded signature data consisting of an array of signers, an array of signatures, and a reference block number. + /// @return The function selector that indicates the signature is valid according to ERC1271 standard. + function isValidSignature( + bytes32 _dataHash, + bytes memory _signatureData + ) external view returns (bytes4) { + ( + address[] memory signers, + bytes[] memory signatures, + uint32 referenceBlock + ) = abi.decode(_signatureData, (address[], bytes[], uint32)); + _checkSignatures(_dataHash, signers, signatures, referenceBlock); + return IERC1271Upgradeable.isValidSignature.selector; + } + + /// @notice Retrieves the current stake quorum details. + /// @return Quorum - The current quorum of strategies and weights + function quorum() external view returns (Quorum memory) { + return _quorum; + } + + /// @notice Retrieves the last recorded weight for a given operator. + /// @param _operator The address of the operator. + /// @return uint256 - The latest weight of the operator. + function getLastCheckpointOperatorWeight( + address _operator + ) external view returns (uint256) { + return _operatorWeightHistory[_operator].latest(); + } + + /// @notice Retrieves the last recorded total weight across all operators. + /// @return uint256 - The latest total weight. + function getLastCheckpointTotalWeight() external view returns (uint256) { + return _totalWeightHistory.latest(); + } + + /// @notice Retrieves the last recorded threshold weight + /// @return uint256 - The latest threshold weight. + function getLastCheckpointThresholdWeight() + external + view + returns (uint256) + { + return _thresholdWeightHistory.latest(); + } + + /// @notice Retrieves the operator's weight at a specific block number. + /// @param _operator The address of the operator. + /// @param _blockNumber The block number to get the operator weight for the quorum + /// @return uint256 - The weight of the operator at the given block. + function getOperatorWeightAtBlock( + address _operator, + uint32 _blockNumber + ) external view returns (uint256) { + return _operatorWeightHistory[_operator].getAtBlock(_blockNumber); + } + + /// @notice Retrieves the total weight at a specific block number. + /// @param _blockNumber The block number to get the total weight for the quorum + /// @return uint256 - The total weight at the given block. + function getLastCheckpointTotalWeightAtBlock( + uint32 _blockNumber + ) external view returns (uint256) { + return _totalWeightHistory.getAtBlock(_blockNumber); + } + + /// @notice Retrieves the threshold weight at a specific block number. + /// @param _blockNumber The block number to get the threshold weight for the quorum + /// @return uint256 - The threshold weight the given block. + function getLastCheckpointThresholdWeightAtBlock( + uint32 _blockNumber + ) external view returns (uint256) { + return _thresholdWeightHistory.getAtBlock(_blockNumber); + } + + function operatorRegistered( + address _operator + ) external view returns (bool) { + return _operatorRegistered[_operator]; + } + + /// @notice Returns the weight an operator must have to contribute to validating an AVS + function minimumWeight() external view returns (uint256) { + return _minimumWeight; + } + + /// @notice Calculates the current weight of an operator based on their delegated stake in the strategies considered in the quorum + /// @param _operator The address of the operator. + /// @return uint256 - The current weight of the operator; returns 0 if below the threshold. + function getOperatorWeight( + address _operator + ) public view returns (uint256) { + StrategyParams[] memory strategyParams = _quorum.strategies; + uint256 weight; + IStrategy[] memory strategies = new IStrategy[](strategyParams.length); + for (uint256 i; i < strategyParams.length; i++) { + strategies[i] = strategyParams[i].strategy; + } + uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares( + _operator, + strategies + ); + for (uint256 i; i < strategyParams.length; i++) { + weight += shares[i] * strategyParams[i].multiplier; + } + weight = weight / BPS; + + if (weight >= _minimumWeight) { + return weight; + } else { + return 0; + } + } + + /// @notice Initializes state for the StakeRegistry + /// @param _serviceManagerAddr The AVS' ServiceManager contract's address + function __ECDSAStakeRegistry_init( + address _serviceManagerAddr, + uint256 _thresholdWeight, + Quorum memory _quorum + ) internal onlyInitializing { + _serviceManager = _serviceManagerAddr; + _updateStakeThreshold(_thresholdWeight); + _updateQuorumConfig(_quorum); + __Ownable_init(); + } + + /// @notice Updates the set of operators for the first quorum. + /// @param operatorsPerQuorum An array of operator address arrays, one for each quorum. + /// @dev This interface maintains compatibility with avs-sync which handles multiquorums while this registry has a single quorum + function updateOperatorsForQuorum( + address[][] memory operatorsPerQuorum, + bytes memory + ) external { + _updateAllOperators(operatorsPerQuorum[0]); + } + + /// @dev Updates the list of operators if the provided list has the correct number of operators. + /// Reverts if the provided list of operators does not match the expected total count of operators. + /// @param _operators The list of operator addresses to update. + function _updateAllOperators(address[] memory _operators) internal { + if (_operators.length != _totalOperators) { + revert MustUpdateAllOperators(); + } + _updateOperators(_operators); + } + + /// @dev Updates the weights for a given list of operator addresses. + /// When passing an operator that isn't registered, then 0 is added to their history + /// @param _operators An array of addresses for which to update the weights. + function _updateOperators(address[] memory _operators) internal { + int256 delta; + for (uint256 i; i < _operators.length; i++) { + delta += _updateOperatorWeight(_operators[i]); + } + _updateTotalWeight(delta); + } + + /// @dev Updates the stake threshold weight and records the history. + /// @param _thresholdWeight The new threshold weight to set and record in the history. + function _updateStakeThreshold(uint256 _thresholdWeight) internal { + _thresholdWeightHistory.push(_thresholdWeight); + emit ThresholdWeightUpdated(_thresholdWeight); + } + + /// @dev Updates the weight an operator must have to join the operator set + /// @param _newMinimumWeight The new weight an operator must have to join the operator set + function _updateMinimumWeight(uint256 _newMinimumWeight) internal { + uint256 oldMinimumWeight = _minimumWeight; + _minimumWeight = _newMinimumWeight; + emit MinimumWeightUpdated(oldMinimumWeight, _newMinimumWeight); + } + + /// @notice Updates the quorum configuration + /// @dev Replaces the current quorum configuration with `_newQuorum` if valid. + /// Reverts with `InvalidQuorum` if the new quorum configuration is not valid. + /// Emits `QuorumUpdated` event with the old and new quorum configurations. + /// @param _newQuorum The new quorum configuration to set. + function _updateQuorumConfig(Quorum memory _newQuorum) internal { + if (!_isValidQuorum(_newQuorum)) { + revert InvalidQuorum(); + } + Quorum memory oldQuorum = _quorum; + delete _quorum; + for (uint256 i; i < _newQuorum.strategies.length; i++) { + _quorum.strategies.push(_newQuorum.strategies[i]); + } + emit QuorumUpdated(oldQuorum, _newQuorum); + } + + /// @dev Internal function to deregister an operator + /// @param _operator The operator's address to deregister + function _deregisterOperator(address _operator) internal { + if (!_operatorRegistered[_operator]) { + revert OperatorNotRegistered(); + } + _totalOperators--; + delete _operatorRegistered[_operator]; + int256 delta = _updateOperatorWeight(_operator); + _updateTotalWeight(delta); + IServiceManager(_serviceManager).deregisterOperatorFromAVS(_operator); + emit OperatorDeregistered(_operator, address(_serviceManager)); + } + + /// @dev registers an operator through a provided signature + /// @param _operatorSignature Contains the operator's signature, salt, and expiry + function _registerOperatorWithSig( + address _operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature + ) internal virtual { + if (_operatorRegistered[_operator]) { + revert OperatorAlreadyRegistered(); + } + _totalOperators++; + _operatorRegistered[_operator] = true; + int256 delta = _updateOperatorWeight(_operator); + _updateTotalWeight(delta); + IServiceManager(_serviceManager).registerOperatorToAVS( + _operator, + _operatorSignature + ); + emit OperatorRegistered(_operator, _serviceManager); + } + + /// @notice Updates the weight of an operator and returns the previous and current weights. + /// @param _operator The address of the operator to update the weight of. + function _updateOperatorWeight( + address _operator + ) internal virtual returns (int256) { + int256 delta; + uint256 newWeight; + uint256 oldWeight = _operatorWeightHistory[_operator].latest(); + if (!_operatorRegistered[_operator]) { + delta -= int256(oldWeight); + if (delta == 0) { + return delta; + } + _operatorWeightHistory[_operator].push(0); + } else { + newWeight = getOperatorWeight(_operator); + delta = int256(newWeight) - int256(oldWeight); + if (delta == 0) { + return delta; + } + _operatorWeightHistory[_operator].push(newWeight); + } + emit OperatorWeightUpdated(_operator, oldWeight, newWeight); + return delta; + } + + /// @dev Internal function to update the total weight of the stake + /// @param delta The change in stake applied last total weight + /// @return oldTotalWeight The weight before the update + /// @return newTotalWeight The updated weight after applying the delta + function _updateTotalWeight( + int256 delta + ) internal returns (uint256 oldTotalWeight, uint256 newTotalWeight) { + oldTotalWeight = _totalWeightHistory.latest(); + int256 newWeight = int256(oldTotalWeight) + delta; + newTotalWeight = uint256(newWeight); + _totalWeightHistory.push(newTotalWeight); + emit TotalWeightUpdated(oldTotalWeight, newTotalWeight); + } + + /** + * @dev Verifies that a specified quorum configuration is valid. A valid quorum has: + * 1. Weights that sum to exactly 10,000 basis points, ensuring proportional representation. + * 2. Unique strategies without duplicates to maintain quorum integrity. + * @param _quorum The quorum configuration to be validated. + * @return bool True if the quorum configuration is valid, otherwise false. + */ + function _isValidQuorum( + Quorum memory _quorum + ) internal pure returns (bool) { + StrategyParams[] memory strategies = _quorum.strategies; + address lastStrategy; + address currentStrategy; + uint256 totalMultiplier; + for (uint256 i; i < strategies.length; i++) { + currentStrategy = address(strategies[i].strategy); + if (lastStrategy >= currentStrategy) revert NotSorted(); + lastStrategy = currentStrategy; + totalMultiplier += strategies[i].multiplier; + } + if (totalMultiplier != BPS) { + return false; + } else { + return true; + } + } + + /** + * @notice Common logic to verify a batch of ECDSA signatures against a hash, using either last stake weight or at a specific block. + * @param _dataHash The hash of the data the signers endorsed. + * @param _signers A collection of addresses that endorsed the data hash. + * @param _signatures A collection of signatures matching the signers. + * @param _referenceBlock The block number for evaluating stake weight; use max uint32 for latest weight. + */ + function _checkSignatures( + bytes32 _dataHash, + address[] memory _signers, + bytes[] memory _signatures, + uint32 _referenceBlock + ) internal view { + uint256 signersLength = _signers.length; + address lastSigner; + uint256 signedWeight; + + _validateSignaturesLength(signersLength, _signatures.length); + for (uint256 i; i < signersLength; i++) { + address currentSigner = _signers[i]; + + _validateSortedSigners(lastSigner, currentSigner); + _validateSignature(currentSigner, _dataHash, _signatures[i]); + + lastSigner = currentSigner; + uint256 operatorWeight = _getOperatorWeight( + currentSigner, + _referenceBlock + ); + signedWeight += operatorWeight; + } + + _validateThresholdStake(signedWeight, _referenceBlock); + } + + /// @notice Validates that the number of signers equals the number of signatures, and neither is zero. + /// @param _signersLength The number of signers. + /// @param _signaturesLength The number of signatures. + function _validateSignaturesLength( + uint256 _signersLength, + uint256 _signaturesLength + ) internal pure { + if (_signersLength != _signaturesLength) { + revert LengthMismatch(); + } + if (_signersLength == 0) { + revert InvalidLength(); + } + } + + /// @notice Ensures that signers are sorted in ascending order by address. + /// @param _lastSigner The address of the last signer. + /// @param _currentSigner The address of the current signer. + function _validateSortedSigners( + address _lastSigner, + address _currentSigner + ) internal pure { + if (_lastSigner >= _currentSigner) { + revert NotSorted(); + } + } + + /// @notice Validates a given signature against the signer's address and data hash. + /// @param _signer The address of the signer to validate. + /// @param _dataHash The hash of the data that is signed. + /// @param _signature The signature to validate. + function _validateSignature( + address _signer, + bytes32 _dataHash, + bytes memory _signature + ) internal view { + if (!_signer.isValidSignatureNow(_dataHash, _signature)) { + revert InvalidSignature(); + } + } + + /// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block. + /// @param _signer The address of the signer whose weight is returned. + /// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint. + /// @return The weight of the operator. + function _getOperatorWeight( + address _signer, + uint32 _referenceBlock + ) internal view returns (uint256) { + if (_referenceBlock == type(uint32).max) { + return _operatorWeightHistory[_signer].latest(); + } else { + return _operatorWeightHistory[_signer].getAtBlock(_referenceBlock); + } + } + + /// @notice Retrieve the total stake weight at a specific block or the latest if not specified. + /// @dev If the `_referenceBlock` is the maximum value for uint32, the latest total weight is returned. + /// @param _referenceBlock The block number to retrieve the total stake weight from. + /// @return The total stake weight at the given block or the latest if the given block is the max uint32 value. + function _getTotalWeight( + uint32 _referenceBlock + ) internal view returns (uint256) { + if (_referenceBlock == type(uint32).max) { + return _totalWeightHistory.latest(); + } else { + return _totalWeightHistory.getAtBlock(_referenceBlock); + } + } + + /// @notice Retrieves the threshold stake for a given reference block. + /// @param _referenceBlock The block number to query the threshold stake for. + /// If set to the maximum uint32 value, it retrieves the latest threshold stake. + /// @return The threshold stake in basis points for the reference block. + function _getThresholdStake( + uint32 _referenceBlock + ) internal view returns (uint256) { + if (_referenceBlock == type(uint32).max) { + return _thresholdWeightHistory.latest(); + } else { + return _thresholdWeightHistory.getAtBlock(_referenceBlock); + } + } + + /// @notice Validates that the cumulative stake of signed messages meets or exceeds the required threshold. + /// @param _signedWeight The cumulative weight of the signers that have signed the message. + /// @param _referenceBlock The block number to verify the stake threshold for + function _validateThresholdStake( + uint256 _signedWeight, + uint32 _referenceBlock + ) internal view { + uint256 totalWeight = _getTotalWeight(_referenceBlock); + if (_signedWeight > totalWeight) { + revert InvalidSignedWeight(); + } + uint256 thresholdStake = _getThresholdStake(_referenceBlock); + if (thresholdStake > _signedWeight) { + revert InsufficientSignedStake(); + } + } +} diff --git a/solidity/contracts/avs/ECDSAStakeRegistryStorage.sol b/solidity/contracts/avs/ECDSAStakeRegistryStorage.sol new file mode 100644 index 0000000000..9e59fd8f63 --- /dev/null +++ b/solidity/contracts/avs/ECDSAStakeRegistryStorage.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import {IDelegationManager} from "../interfaces/avs/vendored/IDelegationManager.sol"; +import {CheckpointsUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/CheckpointsUpgradeable.sol"; +import {IECDSAStakeRegistryEventsAndErrors, Quorum, StrategyParams} from "../interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol"; + +/// @author Layr Labs, Inc. +abstract contract ECDSAStakeRegistryStorage is + IECDSAStakeRegistryEventsAndErrors +{ + /// @notice Manages staking delegations through the DelegationManager interface + IDelegationManager internal immutable DELEGATION_MANAGER; + + /// @dev The total amount of multipliers to weigh stakes + uint256 internal constant BPS = 10_000; + + /// @notice The size of the current operator set + uint256 internal _totalOperators; + + /// @notice Stores the current quorum configuration + Quorum internal _quorum; + + /// @notice Specifies the weight required to become an operator + uint256 internal _minimumWeight; + + /// @notice Holds the address of the service manager + address internal _serviceManager; + + /// @notice Defines the duration after which the stake's weight expires. + uint256 internal _stakeExpiry; + + /// @notice Tracks the total stake history over time using checkpoints + CheckpointsUpgradeable.History internal _totalWeightHistory; + + /// @notice Tracks the threshold bps history using checkpoints + CheckpointsUpgradeable.History internal _thresholdWeightHistory; + + /// @notice Maps operator addresses to their respective stake histories using checkpoints + mapping(address => CheckpointsUpgradeable.History) + internal _operatorWeightHistory; + + /// @notice Maps an operator to their registration status + mapping(address => bool) internal _operatorRegistered; + + /// @param _delegationManager Connects this registry with the DelegationManager + constructor(IDelegationManager _delegationManager) { + DELEGATION_MANAGER = _delegationManager; + } + + // slither-disable-next-line shadowing-state + /// @dev Reserves storage slots for future upgrades + // solhint-disable-next-line + uint256[42] private __gap; +} diff --git a/solidity/contracts/avs/HyperlaneServiceManager.sol b/solidity/contracts/avs/HyperlaneServiceManager.sol new file mode 100644 index 0000000000..b663695ddb --- /dev/null +++ b/solidity/contracts/avs/HyperlaneServiceManager.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {Enrollment, EnrollmentStatus, EnumerableMapEnrollment} from "../libs/EnumerableMapEnrollment.sol"; +import {IAVSDirectory} from "../interfaces/avs/vendored/IAVSDirectory.sol"; +import {IRemoteChallenger} from "../interfaces/avs/IRemoteChallenger.sol"; +import {ISlasher} from "../interfaces/avs/vendored/ISlasher.sol"; +import {ECDSAServiceManagerBase} from "./ECDSAServiceManagerBase.sol"; + +contract HyperlaneServiceManager is ECDSAServiceManagerBase { + // ============ Libraries ============ + + using EnumerableMapEnrollment for EnumerableMapEnrollment.AddressToEnrollmentMap; + + // ============ Public Storage ============ + + // Slasher contract responsible for slashing operators + // @dev slasher needs to be updated once slashing is implemented + ISlasher internal slasher; + + // ============ Events ============ + + /** + * @notice Emitted when an operator is enrolled in a challenger + * @param operator The address of the operator + * @param challenger The address of the challenger + */ + event OperatorEnrolledToChallenger( + address operator, + IRemoteChallenger challenger + ); + + /** + * @notice Emitted when an operator is queued for unenrollment from a challenger + * @param operator The address of the operator + * @param challenger The address of the challenger + * @param unenrollmentStartBlock The block number at which the unenrollment was queued + * @param challengeDelayBlocks The number of blocks to wait before unenrollment is complete + */ + event OperatorQueuedUnenrollmentFromChallenger( + address operator, + IRemoteChallenger challenger, + uint256 unenrollmentStartBlock, + uint256 challengeDelayBlocks + ); + + /** + * @notice Emitted when an operator is unenrolled from a challenger + * @param operator The address of the operator + * @param challenger The address of the challenger + * @param unenrollmentEndBlock The block number at which the unenrollment was completed + */ + event OperatorUnenrolledFromChallenger( + address operator, + IRemoteChallenger challenger, + uint256 unenrollmentEndBlock + ); + + // ============ Internal Storage ============ + + // Mapping of operators to challengers they are enrolled in (enumerable required for remove-all) + mapping(address => EnumerableMapEnrollment.AddressToEnrollmentMap) + internal enrolledChallengers; + + // ============ Modifiers ============ + + // Only allows the challenger the operator is enrolled in to call the function + modifier onlyEnrolledChallenger(address operator) { + (bool exists, ) = enrolledChallengers[operator].tryGet(msg.sender); + require( + exists, + "HyperlaneServiceManager: Operator not enrolled in challenger" + ); + _; + } + + // ============ Constructor ============ + + constructor( + address _avsDirectory, + address _stakeRegistry, + address _paymentCoordinator, + address _delegationManager + ) + ECDSAServiceManagerBase( + _avsDirectory, + _stakeRegistry, + _paymentCoordinator, + _delegationManager + ) + {} + + /** + * @notice Initializes the HyperlaneServiceManager contract with the owner address + */ + function initialize(address _owner) public initializer { + __ServiceManagerBase_init(_owner); + } + + // ============ External Functions ============ + + /** + * @notice Enrolls as an operator into a list of challengers + * @param _challengers The list of challengers to enroll into + */ + function enrollIntoChallengers( + IRemoteChallenger[] memory _challengers + ) external { + for (uint256 i = 0; i < _challengers.length; i++) { + enrollIntoChallenger(_challengers[i]); + } + } + + /** + * @notice starts an operator for unenrollment from a list of challengers + * @param _challengers The list of challengers to unenroll from + */ + function startUnenrollment( + IRemoteChallenger[] memory _challengers + ) external { + for (uint256 i = 0; i < _challengers.length; i++) { + startUnenrollment(_challengers[i]); + } + } + + /** + * @notice Completes the unenrollment of an operator from a list of challengers + * @param _challengers The list of challengers to unenroll from + */ + function completeUnenrollment(address[] memory _challengers) external { + _completeUnenrollment(msg.sender, _challengers); + } + + /** + * @notice Sets the slasher contract responsible for slashing operators + * @param _slasher The address of the slasher contract + */ + function setSlasher(ISlasher _slasher) external onlyOwner { + slasher = _slasher; + } + + /** + * @notice returns the status of a challenger an operator is enrolled in + * @param _operator The address of the operator + * @param _challenger specified IRemoteChallenger contract + */ + function getChallengerEnrollment( + address _operator, + IRemoteChallenger _challenger + ) external view returns (Enrollment memory enrollment) { + return enrolledChallengers[_operator].get(address(_challenger)); + } + + /** + * @notice forwards a call to the Slasher contract to freeze an operator + * @param operator The address of the operator to freeze. + * @dev only the enrolled challengers can call this function + */ + function freezeOperator( + address operator + ) external virtual onlyEnrolledChallenger(operator) { + slasher.freezeOperator(operator); + } + + // ============ Public Functions ============ + + /** + * @notice returns the list of challengers an operator is enrolled in + * @param _operator The address of the operator + */ + function getOperatorChallengers( + address _operator + ) public view returns (address[] memory) { + return enrolledChallengers[_operator].keys(); + } + + /** + * @notice Enrolls as an operator into a single challenger + * @param challenger The challenger to enroll into + */ + function enrollIntoChallenger(IRemoteChallenger challenger) public { + require( + enrolledChallengers[msg.sender].set( + address(challenger), + Enrollment(EnrollmentStatus.ENROLLED, 0) + ) + ); + emit OperatorEnrolledToChallenger(msg.sender, challenger); + } + + /** + * @notice starts an operator for unenrollment from a challenger + * @param challenger The challenger to unenroll from + */ + function startUnenrollment(IRemoteChallenger challenger) public { + (bool exists, Enrollment memory enrollment) = enrolledChallengers[ + msg.sender + ].tryGet(address(challenger)); + require( + exists && enrollment.status == EnrollmentStatus.ENROLLED, + "HyperlaneServiceManager: challenger isn't enrolled" + ); + + enrolledChallengers[msg.sender].set( + address(challenger), + Enrollment( + EnrollmentStatus.PENDING_UNENROLLMENT, + uint248(block.number) + ) + ); + emit OperatorQueuedUnenrollmentFromChallenger( + msg.sender, + challenger, + block.number, + challenger.challengeDelayBlocks() + ); + } + + /** + * @notice Completes the unenrollment of an operator from a challenger + * @param challenger The challenger to unenroll from + */ + function completeUnenrollment(address challenger) public { + _completeUnenrollment(msg.sender, challenger); + } + + // ============ Internal Functions ============ + + /** + * @notice Completes the unenrollment of an operator from a list of challengers + * @param operator The address of the operator + * @param _challengers The list of challengers to unenroll from + */ + function _completeUnenrollment( + address operator, + address[] memory _challengers + ) internal { + for (uint256 i = 0; i < _challengers.length; i++) { + _completeUnenrollment(operator, _challengers[i]); + } + } + + /** + * @notice Completes the unenrollment of an operator from a challenger + * @param operator The address of the operator + * @param _challenger The challenger to unenroll from + */ + function _completeUnenrollment( + address operator, + address _challenger + ) internal { + IRemoteChallenger challenger = IRemoteChallenger(_challenger); + (bool exists, Enrollment memory enrollment) = enrolledChallengers[ + operator + ].tryGet(address(challenger)); + + require( + exists && + enrollment.status == EnrollmentStatus.PENDING_UNENROLLMENT && + block.number >= + enrollment.unenrollmentStartBlock + + challenger.challengeDelayBlocks(), + "HyperlaneServiceManager: Invalid unenrollment" + ); + + enrolledChallengers[operator].remove(address(challenger)); + emit OperatorUnenrolledFromChallenger( + operator, + challenger, + block.number + ); + } + + /// @inheritdoc ECDSAServiceManagerBase + function _deregisterOperatorFromAVS( + address operator + ) internal virtual override { + address[] memory challengers = getOperatorChallengers(operator); + _completeUnenrollment(operator, challengers); + + IAVSDirectory(avsDirectory).deregisterOperatorFromAVS(operator); + emit OperatorDeregisteredFromAVS(operator); + } +} diff --git a/solidity/contracts/hooks/PolygonPosHook.sol b/solidity/contracts/hooks/PolygonPosHook.sol new file mode 100644 index 0000000000..6831f9a332 --- /dev/null +++ b/solidity/contracts/hooks/PolygonPosHook.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol"; +import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol"; +import {TypeCasts} from "../libs/TypeCasts.sol"; +import {Message} from "../libs/Message.sol"; +import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; + +// ============ External Imports ============ +import {FxBaseRootTunnel} from "fx-portal/contracts/tunnel/FxBaseRootTunnel.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title PolygonPosHook + * @notice Message hook to inform the PolygonPosIsm of messages published through + * the native PoS bridge. + */ +contract PolygonPosHook is AbstractMessageIdAuthHook, FxBaseRootTunnel { + using StandardHookMetadata for bytes; + + // ============ Constructor ============ + + constructor( + address _mailbox, + uint32 _destinationDomain, + bytes32 _ism, + address _cpManager, + address _fxRoot + ) + AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) + FxBaseRootTunnel(_cpManager, _fxRoot) + { + require( + Address.isContract(_cpManager), + "PolygonPosHook: invalid cpManager contract" + ); + require( + Address.isContract(_fxRoot), + "PolygonPosHook: invalid fxRoot contract" + ); + } + + // ============ Internal functions ============ + function _quoteDispatch( + bytes calldata, + bytes calldata + ) internal pure override returns (uint256) { + return 0; + } + + /// @inheritdoc AbstractMessageIdAuthHook + function _sendMessageId( + bytes calldata metadata, + bytes memory payload + ) internal override { + require( + metadata.msgValue(0) == 0, + "PolygonPosHook: does not support msgValue" + ); + require(msg.value == 0, "PolygonPosHook: does not support msgValue"); + _sendMessageToChild(payload); + } + + bytes public latestData; + + function _processMessageFromChild(bytes memory data) internal override { + latestData = data; + } +} diff --git a/solidity/contracts/interfaces/avs/IRemoteChallenger.sol b/solidity/contracts/interfaces/avs/IRemoteChallenger.sol new file mode 100644 index 0000000000..0b8ce6bc53 --- /dev/null +++ b/solidity/contracts/interfaces/avs/IRemoteChallenger.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +interface IRemoteChallenger { + /// @notice Returns the number of blocks that must be mined before a challenge can be handled + /// @return The number of blocks that must be mined before a challenge can be handled + function challengeDelayBlocks() external view returns (uint256); + + /// @notice Handles a challenge for an operator + /// @param operator The address of the operator + function handleChallenge(address operator) external; +} diff --git a/solidity/contracts/interfaces/avs/vendored/IAVSDirectory.sol b/solidity/contracts/interfaces/avs/vendored/IAVSDirectory.sol new file mode 100644 index 0000000000..d8003a656f --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/IAVSDirectory.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.0; + +import "./ISignatureUtils.sol"; + +/// part of mock interfaces for vendoring necessary Eigenlayer contracts for the hyperlane AVS +/// @author Layr Labs, Inc. +interface IAVSDirectory is ISignatureUtils { + enum OperatorAVSRegistrationStatus { + UNREGISTERED, + REGISTERED + } + + event AVSMetadataURIUpdated(address indexed avs, string metadataURI); + + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external; + + function deregisterOperatorFromAVS(address operator) external; + + function updateAVSMetadataURI(string calldata metadataURI) external; +} diff --git a/solidity/contracts/interfaces/avs/vendored/IDelegationManager.sol b/solidity/contracts/interfaces/avs/vendored/IDelegationManager.sol new file mode 100644 index 0000000000..8af9f453a3 --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/IDelegationManager.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.0; + +import {IStrategy} from "./IStrategy.sol"; + +/** + * @title DelegationManager + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This is the contract for delegation in EigenLayer. The main functionalities of this contract are + * - enabling anyone to register as an operator in EigenLayer + * - allowing operators to specify parameters related to stakers who delegate to them + * - enabling any staker to delegate its stake to the operator of its choice (a given staker can only delegate to a single operator at a time) + * - enabling a staker to undelegate its assets from the operator it is delegated to (performed as part of the withdrawal process, initiated through the StrategyManager) + */ +interface IDelegationManager { + struct OperatorDetails { + address earningsReceiver; + address delegationApprover; + uint32 stakerOptOutWindowBlocks; + } + + function registerAsOperator( + OperatorDetails calldata registeringOperatorDetails, + string calldata metadataURI + ) external; + + function getOperatorShares( + address operator, + IStrategy[] memory strategies + ) external view returns (uint256[] memory); +} diff --git a/solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol b/solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol new file mode 100644 index 0000000000..021d34db40 --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import {IStrategy} from "./IStrategy.sol"; + +struct StrategyParams { + IStrategy strategy; // The strategy contract reference + uint96 multiplier; // The multiplier applied to the strategy +} + +struct Quorum { + StrategyParams[] strategies; // An array of strategy parameters to define the quorum +} + +/// part of mock interfaces for vendoring necessary Eigenlayer contracts for the hyperlane AVS +/// @author Layr Labs, Inc. +interface IECDSAStakeRegistryEventsAndErrors { + /// @notice Emitted when the system registers an operator + /// @param _operator The address of the registered operator + /// @param _avs The address of the associated AVS + event OperatorRegistered(address indexed _operator, address indexed _avs); + + /// @notice Emitted when the system deregisters an operator + /// @param _operator The address of the deregistered operator + /// @param _avs The address of the associated AVS + event OperatorDeregistered(address indexed _operator, address indexed _avs); + + /// @notice Emitted when the system updates the quorum + /// @param _old The previous quorum configuration + /// @param _new The new quorum configuration + event QuorumUpdated(Quorum _old, Quorum _new); + + /// @notice Emitted when the weight to join the operator set updates + /// @param _old The previous minimum weight + /// @param _new The new minimumWeight + event MinimumWeightUpdated(uint256 _old, uint256 _new); + + /// @notice Emitted when the weight required to be an operator changes + /// @param oldMinimumWeight The previous weight + /// @param newMinimumWeight The updated weight + event UpdateMinimumWeight( + uint256 oldMinimumWeight, + uint256 newMinimumWeight + ); + + /// @notice Emitted when the system updates an operator's weight + /// @param _operator The address of the operator updated + /// @param oldWeight The operator's weight before the update + /// @param newWeight The operator's weight after the update + event OperatorWeightUpdated( + address indexed _operator, + uint256 oldWeight, + uint256 newWeight + ); + + /// @notice Emitted when the system updates the total weight + /// @param oldTotalWeight The total weight before the update + /// @param newTotalWeight The total weight after the update + event TotalWeightUpdated(uint256 oldTotalWeight, uint256 newTotalWeight); + + /// @notice Emits when setting a new threshold weight. + event ThresholdWeightUpdated(uint256 _thresholdWeight); + + /// @notice Indicates when the lengths of the signers array and signatures array do not match. + error LengthMismatch(); + + /// @notice Indicates encountering an invalid length for the signers or signatures array. + error InvalidLength(); + + /// @notice Indicates encountering an invalid signature. + error InvalidSignature(); + + /// @notice Thrown when the threshold update is greater than BPS + error InvalidThreshold(); + + /// @notice Thrown when missing operators in an update + error MustUpdateAllOperators(); + + /// @notice Indicates operator weights were out of sync and the signed weight exceed the total + error InvalidSignedWeight(); + + /// @notice Indicates the total signed stake fails to meet the required threshold. + error InsufficientSignedStake(); + + /// @notice Indicates an individual signer's weight fails to meet the required threshold. + error InsufficientWeight(); + + /// @notice Indicates the quorum is invalid + error InvalidQuorum(); + + /// @notice Indicates the system finds a list of items unsorted + error NotSorted(); + + /// @notice Thrown when registering an already registered operator + error OperatorAlreadyRegistered(); + + /// @notice Thrown when de-registering or updating the stake for an unregistered operator + error OperatorNotRegistered(); +} diff --git a/solidity/contracts/interfaces/avs/vendored/IPaymentCoordinator.sol b/solidity/contracts/interfaces/avs/vendored/IPaymentCoordinator.sol new file mode 100644 index 0000000000..818b84dc73 --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/IPaymentCoordinator.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./IStrategy.sol"; + +/** + * @title Interface for the `IPaymentCoordinator` contract. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice Allows AVSs to make "Range Payments", which get distributed amongst the AVSs' confirmed + * Operators and the Stakers delegated to those Operators. + * Calculations are performed based on the completed Range Payments, with the results posted in + * a Merkle root against which Stakers & Operators can make claims. + */ +interface IPaymentCoordinator { + /// STRUCTS /// + struct StrategyAndMultiplier { + IStrategy strategy; + // weight used to compare shares in multiple strategies against one another + uint96 multiplier; + } + + struct RangePayment { + // Strategies & relative weights of shares in the strategies + StrategyAndMultiplier[] strategiesAndMultipliers; + IERC20 token; + uint256 amount; + uint64 startTimestamp; + uint64 duration; + } + + /// EXTERNAL FUNCTIONS /// + + /** + * @notice Creates a new range payment on behalf of an AVS, to be split amongst the + * set of stakers delegated to operators who are registered to the `avs` + * @param rangePayments The range payments being created + * @dev Expected to be called by the ServiceManager of the AVS on behalf of which the payment is being made + * @dev The duration of the `rangePayment` cannot exceed `MAX_PAYMENT_DURATION` + * @dev The tokens are sent to the `claimingManager` contract + * @dev This function will revert if the `rangePayment` is malformed, + * e.g. if the `strategies` and `weights` arrays are of non-equal lengths + */ + function payForRange(RangePayment[] calldata rangePayments) external; +} diff --git a/solidity/contracts/interfaces/avs/vendored/IServiceManager.sol b/solidity/contracts/interfaces/avs/vendored/IServiceManager.sol new file mode 100644 index 0000000000..2393fc6706 --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/IServiceManager.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.0; + +import {IPaymentCoordinator} from "./IPaymentCoordinator.sol"; +import {IServiceManagerUI} from "./IServiceManagerUI.sol"; + +/** + * @title Minimal interface for a ServiceManager-type contract that forms the single point for an AVS to push updates to EigenLayer + * @author Layr Labs, Inc. + */ +interface IServiceManager is IServiceManagerUI { + /** + * @notice Creates a new range payment on behalf of an AVS, to be split amongst the + * set of stakers delegated to operators who are registered to the `avs`. + * Note that the owner calling this function must have approved the tokens to be transferred to the ServiceManager + * and of course has the required balances. + * @param rangePayments The range payments being created + * @dev Expected to be called by the ServiceManager of the AVS on behalf of which the payment is being made + * @dev The duration of the `rangePayment` cannot exceed `paymentCoordinator.MAX_PAYMENT_DURATION()` + * @dev The tokens are sent to the `PaymentCoordinator` contract + * @dev Strategies must be in ascending order of addresses to check for duplicates + * @dev This function will revert if the `rangePayment` is malformed, + * e.g. if the `strategies` and `weights` arrays are of non-equal lengths + */ + function payForRange( + IPaymentCoordinator.RangePayment[] calldata rangePayments + ) external; +} diff --git a/solidity/contracts/interfaces/avs/vendored/IServiceManagerUI.sol b/solidity/contracts/interfaces/avs/vendored/IServiceManagerUI.sol new file mode 100644 index 0000000000..8df18a409c --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/IServiceManagerUI.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.0; + +import {ISignatureUtils} from "./ISignatureUtils.sol"; +import {IDelegationManager} from "./IDelegationManager.sol"; + +/** + * @title Minimal interface for a ServiceManager-type contract that AVS ServiceManager contracts must implement + * for eigenlabs to be able to index their data on the AVS marketplace frontend. + * @author Layr Labs, Inc. + */ +interface IServiceManagerUI { + /** + * Metadata should follow the format outlined by this example. + * { + * "name": "EigenLabs AVS 1", + * "website": "https://www.eigenlayer.xyz/", + * "description": "This is my 1st AVS", + * "logo": "https://holesky-operator-metadata.s3.amazonaws.com/eigenlayer.png", + * "twitter": "https://twitter.com/eigenlayer" + * } + * @notice Updates the metadata URI for the AVS + * @param _metadataURI is the metadata URI for the AVS + */ + function updateAVSMetadataURI(string memory _metadataURI) external; + + /** + * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator registration with the AVS + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external; + + /** + * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator deregistration from the AVS + * @param operator The address of the operator to deregister. + */ + function deregisterOperatorFromAVS(address operator) external; + + /** + * @notice Returns the list of strategies that the operator has potentially restaked on the AVS + * @param operator The address of the operator to get restaked strategies for + * @dev This function is intended to be called off-chain + * @dev No guarantee is made on whether the operator has shares for a strategy in a quorum or uniqueness + * of each element in the returned array. The off-chain service should do that validation separately + */ + function getOperatorRestakedStrategies( + address operator + ) external view returns (address[] memory); + + /** + * @notice Returns the list of strategies that the AVS supports for restaking + * @dev This function is intended to be called off-chain + * @dev No guarantee is made on uniqueness of each element in the returned array. + * The off-chain service should do that validation separately + */ + function getRestakeableStrategies() + external + view + returns (address[] memory); + + /// @notice Returns the EigenLayer AVSDirectory contract. + function avsDirectory() external view returns (address); +} diff --git a/solidity/contracts/interfaces/avs/vendored/ISignatureUtils.sol b/solidity/contracts/interfaces/avs/vendored/ISignatureUtils.sol new file mode 100644 index 0000000000..158b325d17 --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/ISignatureUtils.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +/** + * @title The interface for common signature utilities. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +interface ISignatureUtils { + // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack management. + struct SignatureWithExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + // @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature. Used primarily for stack management. + struct SignatureWithSaltAndExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the salt used to generate the signature + bytes32 salt; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } +} diff --git a/solidity/contracts/interfaces/avs/vendored/ISlasher.sol b/solidity/contracts/interfaces/avs/vendored/ISlasher.sol new file mode 100644 index 0000000000..577a36eb47 --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/ISlasher.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +/** + * @title Interface for the primary 'slashing' contract for EigenLayer. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +interface ISlasher { + function freezeOperator(address toBeFrozen) external; +} diff --git a/solidity/contracts/interfaces/avs/vendored/IStrategy.sol b/solidity/contracts/interfaces/avs/vendored/IStrategy.sol new file mode 100644 index 0000000000..3bcc517735 --- /dev/null +++ b/solidity/contracts/interfaces/avs/vendored/IStrategy.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title Minimal interface for an `Strategy` contract. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice Custom `Strategy` implementations may expand extensively on this interface. + */ +interface IStrategy { + /** + * @notice Used to deposit tokens into this Strategy + * @param token is the ERC20 token being deposited + * @param amount is the amount of token being deposited + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well. + * @return newShares is the number of new shares issued at the current exchange ratio. + */ + function deposit(IERC20 token, uint256 amount) external returns (uint256); + + /** + * @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address + * @param recipient is the address to receive the withdrawn funds + * @param token is the ERC20 token being transferred out + * @param amountShares is the amount of shares being withdrawn + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * other functions, and individual share balances are recorded in the strategyManager as well. + */ + function withdraw( + address recipient, + IERC20 token, + uint256 amountShares + ) external; + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlying( + uint256 amountShares + ) external returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToShares( + uint256 amountUnderlying + ) external returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlyingView`, this function **may** make state modifications + */ + function userUnderlying(address user) external returns (uint256); + + /** + * @notice convenience function for fetching the current total shares of `user` in this strategy, by + * querying the `strategyManager` contract + */ + function shares(address user) external view returns (uint256); + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlyingView( + uint256 amountShares + ) external view returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToShares`, this function guarantees no state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToSharesView( + uint256 amountUnderlying + ) external view returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications + */ + function userUnderlyingView(address user) external view returns (uint256); + + /// @notice The underlying token for shares in this Strategy + function underlyingToken() external view returns (IERC20); + + /// @notice The total number of extant shares in this Strategy + function totalShares() external view returns (uint256); + + /// @notice Returns either a brief string explaining the strategy's goal & purpose, or a link to metadata that explains in more detail. + function explanation() external view returns (string memory); +} diff --git a/solidity/contracts/isms/hook/PolygonPosIsm.sol b/solidity/contracts/isms/hook/PolygonPosIsm.sol new file mode 100644 index 0000000000..34a40360ee --- /dev/null +++ b/solidity/contracts/isms/hook/PolygonPosIsm.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ + +import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {Message} from "../../libs/Message.sol"; +import {TypeCasts} from "../../libs/TypeCasts.sol"; +import {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol"; + +// ============ External Imports ============ +import {CrossChainEnabledPolygonChild} from "@openzeppelin/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title PolygonPosIsm + * @notice Uses the native Polygon Pos Fx Portal Bridge to verify interchain messages. + */ +contract PolygonPosIsm is + CrossChainEnabledPolygonChild, + AbstractMessageIdAuthorizedIsm +{ + // ============ Constants ============ + + uint8 public constant moduleType = + uint8(IInterchainSecurityModule.Types.NULL); + + // ============ Constructor ============ + + constructor(address _fxChild) CrossChainEnabledPolygonChild(_fxChild) { + require( + Address.isContract(_fxChild), + "PolygonPosIsm: invalid FxChild contract" + ); + } + + // ============ Internal function ============ + + /** + * @notice Check if sender is authorized to message `verifyMessageId`. + */ + function _isAuthorized() internal view override returns (bool) { + return + _crossChainSender() == TypeCasts.bytes32ToAddress(authorizedHook); + } +} diff --git a/solidity/contracts/libs/EnumerableMapEnrollment.sol b/solidity/contracts/libs/EnumerableMapEnrollment.sol new file mode 100644 index 0000000000..857f24f91a --- /dev/null +++ b/solidity/contracts/libs/EnumerableMapEnrollment.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +// ============ External Imports ============ +import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +// ============ Internal Imports ============ +import {TypeCasts} from "./TypeCasts.sol"; + +enum EnrollmentStatus { + UNENROLLED, + ENROLLED, + PENDING_UNENROLLMENT +} + +struct Enrollment { + EnrollmentStatus status; + uint248 unenrollmentStartBlock; +} + +// extends EnumerableMap with address => bytes32 type +// modelled after https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.0/contracts/utils/structs/EnumerableMap.sol +library EnumerableMapEnrollment { + using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; + using EnumerableSet for EnumerableSet.Bytes32Set; + using TypeCasts for address; + using TypeCasts for bytes32; + + struct AddressToEnrollmentMap { + EnumerableMap.Bytes32ToBytes32Map _inner; + } + + // ============ Library Functions ============ + + function encode( + Enrollment memory enrollment + ) public pure returns (bytes32) { + return + bytes32( + abi.encodePacked( + uint8(enrollment.status), + enrollment.unenrollmentStartBlock + ) + ); + } + + function decode(bytes32 encoded) public pure returns (Enrollment memory) { + uint8 status = uint8(encoded[0]); + uint248 unenrollmentStartBlock = uint248(uint256((encoded << 8) >> 8)); + return Enrollment(EnrollmentStatus(status), unenrollmentStartBlock); + } + + function keys( + AddressToEnrollmentMap storage map + ) internal view returns (address[] memory _keys) { + uint256 _length = map._inner.length(); + _keys = new address[](_length); + for (uint256 i = 0; i < _length; i++) { + _keys[i] = address(uint160(uint256(map._inner._keys.at(i)))); + } + } + + function set( + AddressToEnrollmentMap storage map, + address key, + Enrollment memory value + ) internal returns (bool) { + return map._inner.set(key.addressToBytes32(), encode(value)); + } + + function get( + AddressToEnrollmentMap storage map, + address key + ) internal view returns (Enrollment memory) { + return decode(map._inner.get(key.addressToBytes32())); + } + + function tryGet( + AddressToEnrollmentMap storage map, + address key + ) internal view returns (bool, Enrollment memory) { + (bool success, bytes32 value) = map._inner.tryGet( + key.addressToBytes32() + ); + return (success, decode(value)); + } + + function remove( + AddressToEnrollmentMap storage map, + address key + ) internal returns (bool) { + return map._inner.remove(key.addressToBytes32()); + } + + function contains( + AddressToEnrollmentMap storage map, + address key + ) internal view returns (bool) { + return map._inner.contains(key.addressToBytes32()); + } + + function length( + AddressToEnrollmentMap storage map + ) internal view returns (uint256) { + return map._inner.length(); + } + + function at( + AddressToEnrollmentMap storage map, + uint256 index + ) internal view returns (uint256, Enrollment memory) { + (bytes32 key, bytes32 value) = map._inner.at(index); + return (uint256(key), decode(value)); + } +} diff --git a/solidity/contracts/test/ERC20Test.sol b/solidity/contracts/test/ERC20Test.sol index b9bc42f1d9..87f38420e5 100644 --- a/solidity/contracts/test/ERC20Test.sol +++ b/solidity/contracts/test/ERC20Test.sol @@ -3,6 +3,9 @@ pragma solidity >=0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../token/interfaces/IXERC20.sol"; +import "../token/interfaces/IFiatToken.sol"; + contract ERC20Test is ERC20 { uint8 public immutable _decimals; @@ -28,3 +31,38 @@ contract ERC20Test is ERC20 { _mint(account, amount); } } + +contract FiatTokenTest is ERC20Test, IFiatToken { + constructor( + string memory name, + string memory symbol, + uint256 totalSupply, + uint8 __decimals + ) ERC20Test(name, symbol, totalSupply, __decimals) {} + + function burn(uint256 amount) public override { + _burn(msg.sender, amount); + } + + function mint(address account, uint256 amount) public returns (bool) { + _mint(account, amount); + return true; + } +} + +contract XERC20Test is ERC20Test, IXERC20 { + constructor( + string memory name, + string memory symbol, + uint256 totalSupply, + uint8 __decimals + ) ERC20Test(name, symbol, totalSupply, __decimals) {} + + function mint(address account, uint256 amount) public override { + _mint(account, amount); + } + + function burn(address account, uint256 amount) public override { + _burn(account, amount); + } +} diff --git a/solidity/contracts/test/TestRemoteChallenger.sol b/solidity/contracts/test/TestRemoteChallenger.sol new file mode 100644 index 0000000000..e7fbdc220e --- /dev/null +++ b/solidity/contracts/test/TestRemoteChallenger.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IRemoteChallenger} from "../interfaces/avs/IRemoteChallenger.sol"; +import {HyperlaneServiceManager} from "../avs/HyperlaneServiceManager.sol"; + +contract TestRemoteChallenger is IRemoteChallenger { + HyperlaneServiceManager internal immutable hsm; + + constructor(HyperlaneServiceManager _hsm) { + hsm = _hsm; + } + + function challengeDelayBlocks() external pure returns (uint256) { + return 50400; // one week of eth L1 blocks + } + + function handleChallenge(address operator) external { + hsm.freezeOperator(operator); + } +} diff --git a/solidity/contracts/test/avs/TestAVSDirectory.sol b/solidity/contracts/test/avs/TestAVSDirectory.sol new file mode 100644 index 0000000000..ad028d6a82 --- /dev/null +++ b/solidity/contracts/test/avs/TestAVSDirectory.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IAVSDirectory} from "../../interfaces/avs/vendored/IAVSDirectory.sol"; +import {ISignatureUtils} from "../../interfaces/avs/vendored/ISignatureUtils.sol"; +import {ISlasher} from "../../interfaces/avs/vendored/ISlasher.sol"; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +contract TestAVSDirectory is IAVSDirectory { + bytes32 public constant OPERATOR_AVS_REGISTRATION_TYPEHASH = + keccak256( + "OperatorAVSRegistration(address operator,address avs,bytes32 salt,uint256 expiry)" + ); + bytes32 public constant DOMAIN_TYPEHASH = + keccak256( + "EIP712Domain(string name,uint256 chainId,address verifyingContract)" + ); + + mapping(address => mapping(address => OperatorAVSRegistrationStatus)) + public avsOperatorStatus; + + function updateAVSMetadataURI(string calldata metadataURI) external { + emit AVSMetadataURIUpdated(msg.sender, metadataURI); + } + + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external { + bytes32 operatorRegistrationDigestHash = calculateOperatorAVSRegistrationDigestHash({ + operator: operator, + avs: msg.sender, + salt: operatorSignature.salt, + expiry: operatorSignature.expiry + }); + require( + ECDSA.recover( + operatorRegistrationDigestHash, + operatorSignature.signature + ) == operator, + "EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer" + ); + avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus + .REGISTERED; + } + + function deregisterOperatorFromAVS(address operator) external { + avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus + .UNREGISTERED; + } + + function calculateOperatorAVSRegistrationDigestHash( + address operator, + address avs, + bytes32 salt, + uint256 expiry + ) public view returns (bytes32) { + // calculate the struct hash + bytes32 structHash = keccak256( + abi.encode( + OPERATOR_AVS_REGISTRATION_TYPEHASH, + operator, + avs, + salt, + expiry + ) + ); + // calculate the digest hash + bytes32 digestHash = keccak256( + abi.encodePacked("\x19\x01", domainSeparator(), structHash) + ); + return digestHash; + } + + function domainSeparator() public view returns (bytes32) { + return + keccak256( + abi.encode( + DOMAIN_TYPEHASH, + keccak256(bytes("EigenLayer")), + block.chainid, + address(this) + ) + ); + } +} diff --git a/solidity/contracts/test/avs/TestDelegationManager.sol b/solidity/contracts/test/avs/TestDelegationManager.sol new file mode 100644 index 0000000000..cfe4ea2ce6 --- /dev/null +++ b/solidity/contracts/test/avs/TestDelegationManager.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IDelegationManager} from "../../interfaces/avs/vendored/IDelegationManager.sol"; +import {IStrategy} from "../../interfaces/avs/vendored/IStrategy.sol"; + +contract TestDelegationManager is IDelegationManager { + mapping(address => bool) public isOperator; + mapping(address => mapping(IStrategy => uint256)) public operatorShares; + + function registerAsOperator( + OperatorDetails calldata registeringOperatorDetails, + string calldata metadataURI + ) external {} + + function setIsOperator( + address operator, + bool _isOperatorReturnValue + ) external { + isOperator[operator] = _isOperatorReturnValue; + } + + function getOperatorShares( + address operator, + IStrategy[] memory strategies + ) public view returns (uint256[] memory) { + uint256[] memory shares = new uint256[](strategies.length); + for (uint256 i = 0; i < strategies.length; ++i) { + shares[i] = operatorShares[operator][strategies[i]]; + } + return shares; + } +} diff --git a/solidity/contracts/test/avs/TestHyperlaneServiceManager.sol b/solidity/contracts/test/avs/TestHyperlaneServiceManager.sol new file mode 100644 index 0000000000..3a0bafcb99 --- /dev/null +++ b/solidity/contracts/test/avs/TestHyperlaneServiceManager.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {Enrollment, EnrollmentStatus, EnumerableMapEnrollment} from "../../libs/EnumerableMapEnrollment.sol"; +import {HyperlaneServiceManager} from "../../avs/HyperlaneServiceManager.sol"; + +contract TestHyperlaneServiceManager is HyperlaneServiceManager { + using EnumerableMapEnrollment for EnumerableMapEnrollment.AddressToEnrollmentMap; + + constructor( + address _avsDirectory, + address _stakeRegistry, + address _paymentCoordinator, + address _delegationManager + ) + HyperlaneServiceManager( + _avsDirectory, + _stakeRegistry, + _paymentCoordinator, + _delegationManager + ) + {} + + function mockSetUnenrolled(address operator, address challenger) external { + enrolledChallengers[operator].set( + address(challenger), + Enrollment(EnrollmentStatus.UNENROLLED, 0) + ); + } +} diff --git a/solidity/contracts/test/avs/TestPaymentCoordinator.sol b/solidity/contracts/test/avs/TestPaymentCoordinator.sol new file mode 100644 index 0000000000..ac11efe516 --- /dev/null +++ b/solidity/contracts/test/avs/TestPaymentCoordinator.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IPaymentCoordinator} from "../../interfaces/avs/vendored/IPaymentCoordinator.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract TestPaymentCoordinator is IPaymentCoordinator { + using SafeERC20 for IERC20; + + function payForRange(RangePayment[] calldata rangePayments) external { + for (uint256 i = 0; i < rangePayments.length; i++) { + rangePayments[i].token.safeTransferFrom( + msg.sender, + address(this), + rangePayments[i].amount + ); + } + } +} diff --git a/solidity/contracts/test/avs/TestSlasher.sol b/solidity/contracts/test/avs/TestSlasher.sol new file mode 100644 index 0000000000..6f4d3eb1ed --- /dev/null +++ b/solidity/contracts/test/avs/TestSlasher.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {ISlasher} from "../../interfaces/avs/vendored/ISlasher.sol"; + +contract TestSlasher is ISlasher { + function freezeOperator(address toBeFrozen) external {} +} diff --git a/solidity/contracts/token/HypERC20CollateralVaultDeposit.sol b/solidity/contracts/token/extensions/HypERC20CollateralVaultDeposit.sol similarity index 97% rename from solidity/contracts/token/HypERC20CollateralVaultDeposit.sol rename to solidity/contracts/token/extensions/HypERC20CollateralVaultDeposit.sol index 11b530d6ac..27fe09dc08 100644 --- a/solidity/contracts/token/HypERC20CollateralVaultDeposit.sol +++ b/solidity/contracts/token/extensions/HypERC20CollateralVaultDeposit.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.8.0; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; -import {HypERC20Collateral} from "./HypERC20Collateral.sol"; +import {HypERC20Collateral} from "../HypERC20Collateral.sol"; /** * @title Hyperlane ERC20 Token Collateral with deposits collateral to a vault diff --git a/solidity/contracts/token/extensions/HypFiatTokenCollateral.sol b/solidity/contracts/token/extensions/HypFiatTokenCollateral.sol new file mode 100644 index 0000000000..ab043e6413 --- /dev/null +++ b/solidity/contracts/token/extensions/HypFiatTokenCollateral.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IFiatToken} from "../interfaces/IFiatToken.sol"; +import {HypERC20Collateral} from "../HypERC20Collateral.sol"; + +// see https://github.com/circlefin/stablecoin-evm/blob/master/doc/tokendesign.md#issuing-and-destroying-tokens +contract HypFiatTokenCollateral is HypERC20Collateral { + constructor( + address _fiatToken, + address _mailbox + ) HypERC20Collateral(_fiatToken, _mailbox) {} + + function _transferFromSender( + uint256 _amount + ) internal override returns (bytes memory metadata) { + // transfer amount to address(this) + metadata = super._transferFromSender(_amount); + // burn amount of address(this) balance + IFiatToken(address(wrappedToken)).burn(_amount); + } + + function _transferTo( + address _recipient, + uint256 _amount, + bytes calldata /*metadata*/ + ) internal override { + require( + IFiatToken(address(wrappedToken)).mint(_recipient, _amount), + "FiatToken mint failed" + ); + } +} diff --git a/solidity/contracts/token/extensions/HypXERC20Collateral.sol b/solidity/contracts/token/extensions/HypXERC20Collateral.sol new file mode 100644 index 0000000000..f58b526ba3 --- /dev/null +++ b/solidity/contracts/token/extensions/HypXERC20Collateral.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IXERC20} from "../interfaces/IXERC20.sol"; +import {HypERC20Collateral} from "../HypERC20Collateral.sol"; + +contract HypXERC20Collateral is HypERC20Collateral { + constructor( + address _xerc20, + address _mailbox + ) HypERC20Collateral(_xerc20, _mailbox) {} + + function _transferFromSender( + uint256 _amountOrId + ) internal override returns (bytes memory metadata) { + IXERC20(address(wrappedToken)).burn(msg.sender, _amountOrId); + return ""; + } + + function _transferTo( + address _recipient, + uint256 _amountOrId, + bytes calldata /*metadata*/ + ) internal override { + IXERC20(address(wrappedToken)).mint(_recipient, _amountOrId); + } +} diff --git a/solidity/contracts/token/interfaces/IFiatToken.sol b/solidity/contracts/token/interfaces/IFiatToken.sol new file mode 100644 index 0000000000..11a5afa8ce --- /dev/null +++ b/solidity/contracts/token/interfaces/IFiatToken.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +// adapted from https://github.com/circlefin/stablecoin-evm +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IFiatToken is IERC20 { + /** + * @notice Allows a minter to burn some of its own tokens. + * @dev The caller must be a minter, must not be blacklisted, and the amount to burn + * should be less than or equal to the account's balance. + * @param _amount the amount of tokens to be burned. + */ + function burn(uint256 _amount) external; + + /** + * @notice Mints fiat tokens to an address. + * @param _to The address that will receive the minted tokens. + * @param _amount The amount of tokens to mint. Must be less than or equal + * to the minterAllowance of the caller. + * @return True if the operation was successful. + */ + function mint(address _to, uint256 _amount) external returns (bool); +} diff --git a/solidity/contracts/token/interfaces/IXERC20.sol b/solidity/contracts/token/interfaces/IXERC20.sol new file mode 100644 index 0000000000..3f63c477a4 --- /dev/null +++ b/solidity/contracts/token/interfaces/IXERC20.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +// adapted from https://github.com/defi-wonderland/xERC20 + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IXERC20 is IERC20 { + /** + * @notice Mints tokens for a user + * @dev Can only be called by a minter + * @param _user The address of the user who needs tokens minted + * @param _amount The amount of tokens being minted + */ + function mint(address _user, uint256 _amount) external; + + /** + * @notice Burns tokens for a user + * @dev Can only be called by a minter + * @param _user The address of the user who needs tokens burned + * @param _amount The amount of tokens being burned + */ + function burn(address _user, uint256 _amount) external; +} diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 8228539193..8180d9b58f 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -1,5 +1,6 @@ [profile.default] src = 'contracts' +script = 'script' out = 'out' libs = ['node_modules', 'lib'] test = 'test' @@ -9,7 +10,11 @@ solc_version = '0.8.22' evm_version= 'paris' optimizer = true optimizer_runs = 999_999 -fs_permissions = [{ access = "write", path = "fixtures"}] +fs_permissions = [ + { access = "read", path = "./script/avs/"}, + { access = "write", path = "./fixtures" } +] +ignored_warnings_from = ['fx-portal'] [profile.ci] verbosity = 4 @@ -17,3 +22,8 @@ verbosity = 4 [rpc_endpoints] mainnet = "https://eth.merkle.io" optimism = "https://mainnet.optimism.io " +polygon = "https://rpc.ankr.com/polygon" + +[fuzz] +runs = 50 +dictionary_weight = 80 diff --git a/solidity/hardhat.config.cts b/solidity/hardhat.config.cts index 4505567e3f..6d64d971e6 100644 --- a/solidity/hardhat.config.cts +++ b/solidity/hardhat.config.cts @@ -2,6 +2,7 @@ import '@nomiclabs/hardhat-ethers'; import '@nomiclabs/hardhat-waffle'; import '@typechain/hardhat'; import 'hardhat-gas-reporter'; +import 'hardhat-ignore-warnings'; import 'solidity-coverage'; /** @@ -30,4 +31,10 @@ module.exports = { bail: true, import: 'tsx', }, + warnings: { + // turn off all warnings for libs: + 'fx-portal/**/*': { + default: 'off', + }, + }, }; diff --git a/solidity/lib/fx-portal b/solidity/lib/fx-portal new file mode 160000 index 0000000000..ebd046507d --- /dev/null +++ b/solidity/lib/fx-portal @@ -0,0 +1 @@ +Subproject commit ebd046507d76cd03fa2b2559257091471a259ed7 diff --git a/solidity/package.json b/solidity/package.json index 2f956544ae..96077c4339 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -1,13 +1,14 @@ { "name": "@hyperlane-xyz/core", "description": "Core solidity contracts for Hyperlane", - "version": "3.10.0", + "version": "3.11.1", "dependencies": { "@eth-optimism/contracts": "^0.6.0", - "@hyperlane-xyz/utils": "3.10.0", + "@hyperlane-xyz/utils": "3.11.1", "@layerzerolabs/lz-evm-oapp-v2": "2.0.2", "@openzeppelin/contracts": "^4.9.3", - "@openzeppelin/contracts-upgradeable": "^v4.9.3" + "@openzeppelin/contracts-upgradeable": "^v4.9.3", + "fx-portal": "^1.0.3" }, "devDependencies": { "@layerzerolabs/solidity-examples": "^1.1.0", @@ -20,6 +21,7 @@ "ethers": "^5.7.2", "hardhat": "^2.22.2", "hardhat-gas-reporter": "^1.0.9", + "hardhat-ignore-warnings": "^0.2.11", "prettier": "^2.8.8", "prettier-plugin-solidity": "^1.1.3", "solhint": "^4.5.4", diff --git a/solidity/remappings.txt b/solidity/remappings.txt index 1492dd8d72..e85474338a 100644 --- a/solidity/remappings.txt +++ b/solidity/remappings.txt @@ -3,3 +3,4 @@ @eth-optimism=../node_modules/@eth-optimism ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ +fx-portal/=lib/fx-portal/ \ No newline at end of file diff --git a/solidity/script/avs/DeployAVS.s.sol b/solidity/script/avs/DeployAVS.s.sol new file mode 100644 index 0000000000..f2fce2d1d8 --- /dev/null +++ b/solidity/script/avs/DeployAVS.s.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import "forge-std/Script.sol"; + +import {IStrategy} from "../../contracts/interfaces/avs/vendored/IStrategy.sol"; +import {IAVSDirectory} from "../../contracts/interfaces/avs/vendored/IAVSDirectory.sol"; +import {IPaymentCoordinator} from "../../contracts/interfaces/avs/vendored/IPaymentCoordinator.sol"; +import {IDelegationManager} from "../../contracts/interfaces/avs/vendored/IDelegationManager.sol"; + +import {ProxyAdmin} from "../../contracts/upgrade/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from "../../contracts/upgrade/TransparentUpgradeableProxy.sol"; +import {ECDSAStakeRegistry} from "../../contracts/avs/ECDSAStakeRegistry.sol"; +import {Quorum, StrategyParams} from "../../contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol"; +import {HyperlaneServiceManager} from "../../contracts/avs/HyperlaneServiceManager.sol"; + +import {TestPaymentCoordinator} from "../../contracts/test/avs/TestPaymentCoordinator.sol"; + +contract DeployAVS is Script { + using stdJson for string; + + struct StrategyInfo { + string name; + address strategy; + } + + uint256 deployerPrivateKey; + + ProxyAdmin public proxyAdmin; + IAVSDirectory public avsDirectory; + IPaymentCoordinator public paymentCoordinator; + IDelegationManager public delegationManager; + + Quorum quorum; + uint256 thresholdWeight = 6667; + + function _loadEigenlayerAddresses(string memory targetEnv) internal { + string memory root = vm.projectRoot(); + string memory path = string.concat( + root, + "/script/avs/eigenlayer_addresses.json" + ); + string memory json = vm.readFile(path); + + avsDirectory = IAVSDirectory( + json.readAddress( + string(abi.encodePacked(".", targetEnv, ".avsDirectory")) + ) + ); + delegationManager = IDelegationManager( + json.readAddress( + string(abi.encodePacked(".", targetEnv, ".delegationManager")) + ) + ); + // paymentCoordinator = IPaymentCoordinator(json.readAddress(string(abi.encodePacked(".", targetEnv, ".paymentCoordinator")))); + paymentCoordinator = new TestPaymentCoordinator(); // temporary until Eigenlayer deploys the real one + + StrategyInfo[] memory strategies = abi.decode( + vm.parseJson( + json, + string(abi.encodePacked(".", targetEnv, ".strategies")) + ), + (StrategyInfo[]) + ); + + StrategyParams memory strategyParam; + + uint96 totalMultipliers = 10_000; + uint96 multiplier; + + uint96 strategyCount = uint96(strategies.length); + for (uint96 i = 0; i < strategyCount; i++) { + // the multipliers need to add up to 10,000, so we divide the total by the number of strategies for the first n-1 strategies + // and then the last strategy gets the remainder + if (i < strategyCount - 1) { + multiplier = totalMultipliers / uint96(strategyCount); + } else { + multiplier = + totalMultipliers - + multiplier * + uint96(strategyCount - 1); + } + strategyParam = StrategyParams({ + strategy: IStrategy(strategies[i].strategy), + multiplier: multiplier + }); + quorum.strategies.push(strategyParam); + } + } + + function run(string memory network) external { + deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + _loadEigenlayerAddresses(network); + + vm.startBroadcast(deployerPrivateKey); + + proxyAdmin = new ProxyAdmin(); + + ECDSAStakeRegistry stakeRegistryImpl = new ECDSAStakeRegistry( + delegationManager + ); + TransparentUpgradeableProxy stakeRegistryProxy = new TransparentUpgradeableProxy( + address(stakeRegistryImpl), + address(proxyAdmin), + "" + ); + + HyperlaneServiceManager strategyManagerImpl = new HyperlaneServiceManager( + address(avsDirectory), + address(stakeRegistryProxy), + address(paymentCoordinator), + address(delegationManager) + ); + + TransparentUpgradeableProxy hsmProxy = new TransparentUpgradeableProxy( + address(strategyManagerImpl), + address(proxyAdmin), + abi.encodeWithSelector( + HyperlaneServiceManager.initialize.selector, + msg.sender + ) + ); + + // Initialize the ECDSAStakeRegistry once we have the HyperlaneServiceManager proxy + (bool success, ) = address(stakeRegistryProxy).call( + abi.encodeWithSelector( + ECDSAStakeRegistry.initialize.selector, + address(hsmProxy), + thresholdWeight, + quorum + ) + ); + require(success, "Failed to initialize ECDSAStakeRegistry"); + + console.log( + "ECDSAStakeRegistry Implementation: ", + address(stakeRegistryImpl) + ); + console.log( + "HyperlaneServiceManager Implementation: ", + address(strategyManagerImpl) + ); + console.log("StakeRegistry Proxy: ", address(stakeRegistryProxy)); + console.log("HyperlaneServiceManager Proxy: ", address(hsmProxy)); + + vm.stopBroadcast(); + } +} diff --git a/solidity/script/avs/eigenlayer_addresses.json b/solidity/script/avs/eigenlayer_addresses.json new file mode 100644 index 0000000000..d8890a77be --- /dev/null +++ b/solidity/script/avs/eigenlayer_addresses.json @@ -0,0 +1,40 @@ +{ + "ethereum": { + "delegationManager": "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A", + "avsDirectory": "0x135DDa560e946695d6f155dACaFC6f1F25C1F5AF", + "paymentCoordinator": "", + "strategies": [ + { + "name": "cbETH", + "strategy": "0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc" + }, + { + "name": "stETH", + "strategy": "0x93c4b944D05dfe6df7645A86cd2206016c51564D" + }, + { + "name": "Beacon Chain ETH", + "strategy": "0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0" + } + ] + }, + "holesky": { + "delegationManager": "0xA44151489861Fe9e3055d95adC98FbD462B948e7", + "avsDirectory": "0x055733000064333CaDDbC92763c58BF0192fFeBf", + "paymentCoordinator": "", + "strategies": [ + { + "name": "cbETH", + "strategy": "0x70EB4D3c164a6B4A5f908D4FBb5a9cAfFb66bAB6" + }, + { + "name": "stETH", + "strategy": "0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3" + }, + { + "name": "Beacon Chain ETH", + "strategy": "0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0" + } + ] + } +} diff --git a/solidity/test/Router.t.sol b/solidity/test/Router.t.sol index b4b095a504..bd6150824e 100644 --- a/solidity/test/Router.t.sol +++ b/solidity/test/Router.t.sol @@ -97,6 +97,7 @@ contract RouterTest is Test { address notOwner, bytes32 remoteRouter ) public { + vm.assume(notOwner != router.owner()); vm.prank(notOwner); vm.expectRevert(bytes("Ownable: caller is not the owner")); router.enrollRemoteRouter(origin, remoteRouter); diff --git a/solidity/test/avs/EigenlayerBase.sol b/solidity/test/avs/EigenlayerBase.sol new file mode 100644 index 0000000000..60adf3a9f9 --- /dev/null +++ b/solidity/test/avs/EigenlayerBase.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; + +import {ISlasher} from "../../contracts/interfaces/avs/vendored/ISlasher.sol"; +import {TestAVSDirectory} from "../../contracts/test/avs/TestAVSDirectory.sol"; +import {TestDelegationManager} from "../../contracts/test/avs/TestDelegationManager.sol"; +import {TestSlasher} from "../../contracts/test/avs/TestSlasher.sol"; + +contract EigenlayerBase is Test { + TestAVSDirectory internal avsDirectory; + TestDelegationManager internal delegationManager; + ISlasher internal slasher; + + function _deployMockEigenLayerAndAVS() internal { + avsDirectory = new TestAVSDirectory(); + delegationManager = new TestDelegationManager(); + slasher = new TestSlasher(); + } +} diff --git a/solidity/test/avs/HyperlaneServiceManager.t.sol b/solidity/test/avs/HyperlaneServiceManager.t.sol new file mode 100644 index 0000000000..4ea9ce8f1e --- /dev/null +++ b/solidity/test/avs/HyperlaneServiceManager.t.sol @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IDelegationManager} from "../../contracts/interfaces/avs/vendored/IDelegationManager.sol"; +import {ISlasher} from "../../contracts/interfaces/avs/vendored/ISlasher.sol"; + +import {IAVSDirectory} from "../../contracts/interfaces/avs/vendored/IAVSDirectory.sol"; +import {Quorum, StrategyParams} from "../../contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol"; +import {TestDelegationManager} from "../../contracts/test/avs/TestDelegationManager.sol"; +import {ECDSAStakeRegistry} from "../../contracts/avs/ECDSAStakeRegistry.sol"; +import {TestPaymentCoordinator} from "../../contracts/test/avs/TestPaymentCoordinator.sol"; + +import {IStrategy} from "../../contracts/interfaces/avs/vendored/IStrategy.sol"; +import {ISignatureUtils} from "../../contracts/interfaces/avs/vendored/ISignatureUtils.sol"; +import {Enrollment, EnrollmentStatus} from "../../contracts/libs/EnumerableMapEnrollment.sol"; +import {IRemoteChallenger} from "../../contracts/interfaces/avs/IRemoteChallenger.sol"; + +import {HyperlaneServiceManager} from "../../contracts/avs/HyperlaneServiceManager.sol"; +import {TestHyperlaneServiceManager} from "../../contracts/test/avs/TestHyperlaneServiceManager.sol"; +import {TestRemoteChallenger} from "../../contracts/test/TestRemoteChallenger.sol"; + +import {EigenlayerBase} from "./EigenlayerBase.sol"; + +contract HyperlaneServiceManagerTest is EigenlayerBase { + TestHyperlaneServiceManager internal _hsm; + ECDSAStakeRegistry internal _ecdsaStakeRegistry; + TestPaymentCoordinator internal _paymentCoordinator; + + // Operator info + uint256 operatorPrivateKey = 0xdeadbeef; + address operator; + + bytes32 emptySalt; + uint256 maxExpiry = type(uint256).max; + uint256 challengeDelayBlocks = 50400; // one week of eth L1 blocks + address invalidServiceManager = address(0x1234); + + function setUp() public { + _deployMockEigenLayerAndAVS(); + + _ecdsaStakeRegistry = new ECDSAStakeRegistry(delegationManager); + _paymentCoordinator = new TestPaymentCoordinator(); + + _hsm = new TestHyperlaneServiceManager( + address(avsDirectory), + address(_ecdsaStakeRegistry), + address(_paymentCoordinator), + address(delegationManager) + ); + _hsm.initialize(address(this)); + _hsm.setSlasher(slasher); + + IStrategy mockStrategy = IStrategy(address(0x1234)); + Quorum memory quorum = Quorum({strategies: new StrategyParams[](1)}); + quorum.strategies[0] = StrategyParams({ + strategy: mockStrategy, + multiplier: 10000 + }); + _ecdsaStakeRegistry.initialize(address(_hsm), 6667, quorum); + + // register operator to eigenlayer + operator = vm.addr(operatorPrivateKey); + vm.prank(operator); + delegationManager.registerAsOperator( + IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }), + "" + ); + // set operator as registered in Eigenlayer + delegationManager.setIsOperator(operator, true); + } + + event AVSMetadataURIUpdated(address indexed avs, string metadataURI); + + function test_updateAVSMetadataURI() public { + vm.expectEmit(true, true, true, true, address(avsDirectory)); + emit AVSMetadataURIUpdated(address(_hsm), "hyperlaneAVS"); + _hsm.updateAVSMetadataURI("hyperlaneAVS"); + } + + function test_updateAVSMetadataURI_revert_notOwnable() public { + vm.prank(address(0x1234)); + vm.expectRevert("Ownable: caller is not the owner"); + _hsm.updateAVSMetadataURI("hyperlaneAVS"); + } + + function test_registerOperator() public { + // act + ISignatureUtils.SignatureWithSaltAndExpiry + memory operatorSignature = _getOperatorSignature( + operatorPrivateKey, + operator, + address(_hsm), + emptySalt, + maxExpiry + ); + _ecdsaStakeRegistry.registerOperatorWithSignature( + operator, + operatorSignature + ); + + // assert + IAVSDirectory.OperatorAVSRegistrationStatus operatorStatus = avsDirectory + .avsOperatorStatus(address(_hsm), operator); + assertEq( + uint8(operatorStatus), + uint8(IAVSDirectory.OperatorAVSRegistrationStatus.REGISTERED) + ); + } + + function test_registerOperator_revert_invalidSignature() public { + // act + ISignatureUtils.SignatureWithSaltAndExpiry + memory operatorSignature = _getOperatorSignature( + operatorPrivateKey, + operator, + address(0x1), + emptySalt, + maxExpiry + ); + + vm.expectRevert( + "EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer" + ); + _ecdsaStakeRegistry.registerOperatorWithSignature( + operator, + operatorSignature + ); + + // assert + IAVSDirectory.OperatorAVSRegistrationStatus operatorStatus = avsDirectory + .avsOperatorStatus(address(_hsm), operator); + assertEq( + uint8(operatorStatus), + uint8(IAVSDirectory.OperatorAVSRegistrationStatus.UNREGISTERED) + ); + } + + function test_deregisterOperator() public { + // act + _registerOperator(); + vm.prank(operator); + _ecdsaStakeRegistry.deregisterOperator(); + + // assert + IAVSDirectory.OperatorAVSRegistrationStatus operatorStatus = avsDirectory + .avsOperatorStatus(address(_hsm), operator); + assertEq( + uint8(operatorStatus), + uint8(IAVSDirectory.OperatorAVSRegistrationStatus.UNREGISTERED) + ); + } + + function testFuzz_enrollIntoChallengers(uint8 numOfChallengers) public { + _registerOperator(); + IRemoteChallenger[] memory challengers = _deployChallengers( + numOfChallengers + ); + + vm.prank(operator); + _hsm.enrollIntoChallengers(challengers); + + _assertChallengers(challengers, EnrollmentStatus.ENROLLED, 0); + } + + // to check if the exists in tryGet is working for when we set the enrollment to UNENROLLED + function test_checkUnenrolled() public { + _registerOperator(); + IRemoteChallenger[] memory challengers = _deployChallengers(1); + + vm.prank(operator); + _hsm.enrollIntoChallengers(challengers); + _assertChallengers(challengers, EnrollmentStatus.ENROLLED, 0); + + _hsm.mockSetUnenrolled(operator, address(challengers[0])); + _assertChallengers(challengers, EnrollmentStatus.UNENROLLED, 0); + } + + function testFuzz_startUnenrollment_revert(uint8 numOfChallengers) public { + vm.assume(numOfChallengers > 0); + + _registerOperator(); + IRemoteChallenger[] memory challengers = _deployChallengers( + numOfChallengers + ); + + vm.startPrank(operator); + + vm.expectRevert("HyperlaneServiceManager: challenger isn't enrolled"); + _hsm.startUnenrollment(challengers); + + _hsm.enrollIntoChallengers(challengers); + _hsm.startUnenrollment(challengers); + _assertChallengers( + challengers, + EnrollmentStatus.PENDING_UNENROLLMENT, + block.number + ); + + vm.expectRevert("HyperlaneServiceManager: challenger isn't enrolled"); + _hsm.startUnenrollment(challengers); + _assertChallengers( + challengers, + EnrollmentStatus.PENDING_UNENROLLMENT, + block.number + ); + + vm.stopPrank(); + } + + function testFuzz_startUnenrollment( + uint8 numOfChallengers, + uint8 numQueued + ) public { + vm.assume(numQueued <= numOfChallengers); + + _registerOperator(); + IRemoteChallenger[] memory challengers = _deployChallengers( + numOfChallengers + ); + IRemoteChallenger[] memory queuedChallengers = new IRemoteChallenger[]( + numQueued + ); + for (uint8 i = 0; i < numQueued; i++) { + queuedChallengers[i] = challengers[i]; + } + IRemoteChallenger[] + memory unqueuedChallengers = new IRemoteChallenger[]( + numOfChallengers - numQueued + ); + for (uint8 i = numQueued; i < numOfChallengers; i++) { + unqueuedChallengers[i - numQueued] = challengers[i]; + } + + vm.startPrank(operator); + _hsm.enrollIntoChallengers(challengers); + _assertChallengers(challengers, EnrollmentStatus.ENROLLED, 0); + + _hsm.startUnenrollment(queuedChallengers); + _assertChallengers( + queuedChallengers, + EnrollmentStatus.PENDING_UNENROLLMENT, + block.number + ); + _assertChallengers(unqueuedChallengers, EnrollmentStatus.ENROLLED, 0); + + vm.stopPrank(); + } + + function testFuzz_completeQueuedUnenrollmentFromChallenger( + uint8 numOfChallengers, + uint8 numUnenrollable + ) public { + vm.assume(numUnenrollable > 0 && numUnenrollable <= numOfChallengers); + + _registerOperator(); + IRemoteChallenger[] memory challengers = _deployChallengers( + numOfChallengers + ); + address[] memory unenrollableChallengers = new address[]( + numUnenrollable + ); + for (uint8 i = 0; i < numUnenrollable; i++) { + unenrollableChallengers[i] = address(challengers[i]); + } + + vm.startPrank(operator); + _hsm.enrollIntoChallengers(challengers); + _hsm.startUnenrollment(challengers); + + _assertChallengers( + challengers, + EnrollmentStatus.PENDING_UNENROLLMENT, + block.number + ); + + vm.expectRevert(); + _hsm.completeUnenrollment(unenrollableChallengers); + + vm.roll(block.number + challengeDelayBlocks); + + _hsm.completeUnenrollment(unenrollableChallengers); + + assertEq( + _hsm.getOperatorChallengers(operator).length, + numOfChallengers - numUnenrollable + ); + + vm.stopPrank(); + } + + function testFuzz_freezeOperator(uint8 numOfChallengers) public { + _registerOperator(); + + IRemoteChallenger[] memory challengers = _deployChallengers( + numOfChallengers + ); + + vm.prank(operator); + _hsm.enrollIntoChallengers(challengers); + + for (uint256 i = 0; i < challengers.length; i++) { + vm.expectCall( + address(slasher), + abi.encodeCall(ISlasher.freezeOperator, (operator)) + ); + challengers[i].handleChallenge(operator); + } + } + + function testFuzz_freezeOperator_duringEnrollment( + uint8 numOfChallengers, + uint8 numUnenrollable + ) public { + vm.assume(numUnenrollable > 0 && numUnenrollable <= numOfChallengers); + + _registerOperator(); + IRemoteChallenger[] memory challengers = _deployChallengers( + numOfChallengers + ); + address[] memory unenrollableChallengers = new address[]( + numUnenrollable + ); + IRemoteChallenger[] + memory otherChallengeChallengers = new IRemoteChallenger[]( + numOfChallengers - numUnenrollable + ); + for (uint8 i = 0; i < numUnenrollable; i++) { + unenrollableChallengers[i] = address(challengers[i]); + } + for (uint8 i = numUnenrollable; i < numOfChallengers; i++) { + otherChallengeChallengers[i - numUnenrollable] = challengers[i]; + } + + vm.startPrank(operator); + _hsm.enrollIntoChallengers(challengers); + + for (uint256 i = 0; i < challengers.length; i++) { + vm.expectCall( + address(slasher), + abi.encodeCall(ISlasher.freezeOperator, (operator)) + ); + challengers[i].handleChallenge(operator); + } + + _hsm.startUnenrollment(challengers); + vm.roll(block.number + challengeDelayBlocks); + _hsm.completeUnenrollment(unenrollableChallengers); + + for (uint256 i = 0; i < unenrollableChallengers.length; i++) { + vm.expectRevert( + "HyperlaneServiceManager: Operator not enrolled in challenger" + ); + IRemoteChallenger(unenrollableChallengers[i]).handleChallenge( + operator + ); + } + for (uint256 i = 0; i < otherChallengeChallengers.length; i++) { + vm.expectCall( + address(slasher), + abi.encodeCall(ISlasher.freezeOperator, (operator)) + ); + otherChallengeChallengers[i].handleChallenge(operator); + } + vm.stopPrank(); + } + + function testFuzz_deregisterOperator_withEnrollment() public { + uint8 numOfChallengers = 1; + vm.assume(numOfChallengers > 0); + + _registerOperator(); + IRemoteChallenger[] memory challengers = _deployChallengers( + numOfChallengers + ); + + vm.startPrank(operator); + _hsm.enrollIntoChallengers(challengers); + _assertChallengers(challengers, EnrollmentStatus.ENROLLED, 0); + + vm.expectRevert("HyperlaneServiceManager: Invalid unenrollment"); + _ecdsaStakeRegistry.deregisterOperator(); + + _hsm.startUnenrollment(challengers); + + vm.expectRevert("HyperlaneServiceManager: Invalid unenrollment"); + _ecdsaStakeRegistry.deregisterOperator(); + + vm.roll(block.number + challengeDelayBlocks); + + _ecdsaStakeRegistry.deregisterOperator(); + + assertEq(_hsm.getOperatorChallengers(operator).length, 0); + vm.stopPrank(); + } + + // ============ Utility Functions ============ + + function _registerOperator() internal { + ISignatureUtils.SignatureWithSaltAndExpiry + memory operatorSignature = _getOperatorSignature( + operatorPrivateKey, + operator, + address(_hsm), + emptySalt, + maxExpiry + ); + + _ecdsaStakeRegistry.registerOperatorWithSignature( + operator, + operatorSignature + ); + } + + function _deployChallengers( + uint8 numOfChallengers + ) internal returns (IRemoteChallenger[] memory challengers) { + challengers = new IRemoteChallenger[](numOfChallengers); + for (uint8 i = 0; i < numOfChallengers; i++) { + challengers[i] = new TestRemoteChallenger(_hsm); + } + } + + function _assertChallengers( + IRemoteChallenger[] memory _challengers, + EnrollmentStatus _expectedstatus, + uint256 _expectUnenrollmentBlock + ) internal { + for (uint256 i = 0; i < _challengers.length; i++) { + Enrollment memory enrollment = _hsm.getChallengerEnrollment( + operator, + _challengers[i] + ); + assertEq(uint8(enrollment.status), uint8(_expectedstatus)); + if (_expectUnenrollmentBlock != 0) { + assertEq( + enrollment.unenrollmentStartBlock, + _expectUnenrollmentBlock + ); + } + } + } + + function _getOperatorSignature( + uint256 _operatorPrivateKey, + address operatorToSign, + address avs, + bytes32 salt, + uint256 expiry + ) + internal + view + returns ( + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) + { + operatorSignature.salt = salt; + operatorSignature.expiry = expiry; + { + bytes32 digestHash = avsDirectory + .calculateOperatorAVSRegistrationDigestHash( + operatorToSign, + avs, + salt, + expiry + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _operatorPrivateKey, + digestHash + ); + operatorSignature.signature = abi.encodePacked(r, s, v); + } + return operatorSignature; + } +} diff --git a/solidity/test/isms/AggregationIsm.t.sol b/solidity/test/isms/AggregationIsm.t.sol index 16b34a3070..8806d9b66c 100644 --- a/solidity/test/isms/AggregationIsm.t.sol +++ b/solidity/test/isms/AggregationIsm.t.sol @@ -14,6 +14,8 @@ contract AggregationIsmTest is Test { using Strings for uint256; using Strings for uint8; + string constant fixtureKey = "fixture"; + StaticAggregationIsmFactory factory; IAggregationIsm ism; @@ -21,6 +23,24 @@ contract AggregationIsmTest is Test { factory = new StaticAggregationIsmFactory(); } + function fixtureAppendMetadata( + uint256 index, + bytes memory metadata + ) internal { + vm.serializeBytes(fixtureKey, index.toString(), metadata); + } + + function fixtureAppendNull(uint256 index) internal { + vm.serializeString(fixtureKey, index.toString(), "null"); + } + + function writeFixture(bytes memory metadata, uint8 m) internal { + string memory path = string( + abi.encodePacked("./fixtures/aggregation/", m.toString(), ".json") + ); + vm.writeJson(vm.serializeBytes(fixtureKey, "encoded", metadata), path); + } + function deployIsms( uint8 m, uint8 n, @@ -44,33 +64,28 @@ contract AggregationIsmTest is Test { uint32 start = 8 * uint32(choices.length); bytes memory metametadata; - string memory structured = "structured"; for (uint256 i = 0; i < choices.length; i++) { bool included = false; for (uint256 j = 0; j < chosen.length; j++) { included = included || choices[i] == chosen[j]; } - bytes memory metadata = ""; if (included) { - metadata = TestIsm(choices[i]).requiredMetadata(); + bytes memory metadata = TestIsm(choices[i]).requiredMetadata(); uint32 end = start + uint32(metadata.length); uint64 offset = (uint64(start) << 32) | uint64(end); offsets = bytes.concat(offsets, abi.encodePacked(offset)); start = end; metametadata = abi.encodePacked(metametadata, metadata); - vm.serializeBytes(structured, i.toString(), metadata); + fixtureAppendMetadata(i, metadata); } else { offsets = bytes.concat(offsets, abi.encodePacked(uint64(0))); - vm.serializeString(structured, i.toString(), "null"); + fixtureAppendNull(i); } } - string memory path = string( - abi.encodePacked("./fixtures/aggregation/", m.toString(), ".json") - ); bytes memory encoded = abi.encodePacked(offsets, metametadata); - vm.writeJson(vm.serializeBytes(structured, "encoded", encoded), path); + writeFixture(encoded, m); return encoded; } diff --git a/solidity/test/isms/MultisigIsm.t.sol b/solidity/test/isms/MultisigIsm.t.sol index 124d152e19..6cf8c7036e 100644 --- a/solidity/test/isms/MultisigIsm.t.sol +++ b/solidity/test/isms/MultisigIsm.t.sol @@ -25,6 +25,11 @@ abstract contract AbstractMultisigIsmTest is Test { using Strings for uint256; using Strings for uint8; + string constant fixtureKey = "fixture"; + string constant signatureKey = "signature"; + string constant signaturesKey = "signatures"; + string constant prefixKey = "prefix"; + uint32 constant ORIGIN = 11; StaticThresholdAddressSetFactory factory; IMultisigIsm ism; @@ -34,7 +39,47 @@ abstract contract AbstractMultisigIsmTest is Test { function metadataPrefix( bytes memory message - ) internal virtual returns (bytes memory, string memory); + ) internal virtual returns (bytes memory); + + function fixtureInit() internal { + vm.serializeUint(fixtureKey, "type", uint256(ism.moduleType())); + string memory prefix = vm.serializeString(prefixKey, "dummy", "dummy"); + vm.serializeString(fixtureKey, "prefix", prefix); + } + + function fixtureAppendSignature( + uint256 index, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + vm.serializeUint(signatureKey, "v", uint256(v)); + vm.serializeBytes32(signatureKey, "r", r); + string memory signature = vm.serializeBytes32(signatureKey, "s", s); + vm.serializeString(signaturesKey, index.toString(), signature); + } + + function writeFixture(bytes memory metadata, uint8 m, uint8 n) internal { + vm.serializeString( + fixtureKey, + "signatures", + vm.serializeString(signaturesKey, "dummy", "dummy") + ); + + string memory fixturePath = string( + abi.encodePacked( + "./fixtures/multisig/", + m.toString(), + "-", + n.toString(), + ".json" + ) + ); + vm.writeJson( + vm.serializeBytes(fixtureKey, "encoded", metadata), + fixturePath + ); + } function getMetadata( uint8 m, @@ -42,8 +87,6 @@ abstract contract AbstractMultisigIsmTest is Test { bytes32 seed, bytes memory message ) internal returns (bytes memory) { - string memory structured = "structured"; - bytes32 digest; { uint32 domain = mailbox.localDomain(); @@ -66,40 +109,18 @@ abstract contract AbstractMultisigIsmTest is Test { seed ); - vm.serializeUint(structured, "type", uint256(ism.moduleType())); - - (bytes memory metadata, string memory prefix) = metadataPrefix(message); - vm.serializeString(structured, "prefix", prefix); + bytes memory metadata = metadataPrefix(message); + fixtureInit(); - string memory signatures = "signatures"; for (uint256 i = 0; i < m; i++) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signers[i], digest); metadata = abi.encodePacked(metadata, r, s, v); - string memory signature = "signature"; - vm.serializeUint(signature, "v", uint256(v)); - vm.serializeBytes32(signature, "r", r); - signature = vm.serializeBytes32(signature, "s", s); - vm.serializeString(signatures, i.toString(), signature); + fixtureAppendSignature(i, v, r, s); } - vm.serializeString( - structured, - "signatures", - vm.serializeString(signatures, "dummy", "dummy") // idk - ); - - string memory path = string( - abi.encodePacked( - "./fixtures/multisig/", - m.toString(), - "-", - n.toString(), - ".json" - ) - ); + writeFixture(metadata, m, n); - vm.writeJson(vm.serializeBytes(structured, "encoded", metadata), path); return metadata; } @@ -171,6 +192,8 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { using Message for bytes; using Strings for uint256; + string constant proofKey = "proof"; + function setUp() public { mailbox = new TestMailbox(ORIGIN); merkleTreeHook = new TestMerkleTreeHook(address(mailbox)); @@ -180,34 +203,47 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { mailbox.setRequiredHook(address(noopHook)); } + function fixturePrefix( + uint32 checkpointIndex, + bytes32 merkleTreeAddress, + bytes32 messageId, + bytes32[32] memory proof + ) internal { + vm.serializeUint(prefixKey, "index", uint256(checkpointIndex)); + vm.serializeBytes32(prefixKey, "merkleTree", merkleTreeAddress); + vm.serializeUint(prefixKey, "signedIndex", uint256(checkpointIndex)); + vm.serializeBytes32(prefixKey, "id", messageId); + + for (uint256 i = 0; i < 32; i++) { + vm.serializeBytes32(proofKey, i.toString(), proof[i]); + } + string memory proofString = vm.serializeString( + proofKey, + "dummy", + "dummy" + ); + vm.serializeString(prefixKey, "proof", proofString); + } + // TODO: test merkleIndex != signedIndex function metadataPrefix( bytes memory message - ) internal override returns (bytes memory metadata, string memory prefix) { + ) internal override returns (bytes memory) { uint32 checkpointIndex = uint32(merkleTreeHook.count() - 1); bytes32[32] memory proof = merkleTreeHook.proof(); bytes32 messageId = message.id(); bytes32 merkleTreeAddress = address(merkleTreeHook).addressToBytes32(); - prefix = "prefix"; - vm.serializeUint(prefix, "index", uint256(checkpointIndex)); - vm.serializeUint(prefix, "signedIndex", uint256(checkpointIndex)); - vm.serializeBytes32(prefix, "merkleTree", merkleTreeAddress); - string memory proofString = "proof"; - for (uint256 i = 0; i < 32; i++) { - vm.serializeBytes32(proofString, i.toString(), proof[i]); - } - proofString = vm.serializeString(proofString, "dummy", "dummy"); - vm.serializeString(prefix, "proof", proofString); - prefix = vm.serializeBytes32(prefix, "id", messageId); - - metadata = abi.encodePacked( - merkleTreeAddress, - checkpointIndex, - messageId, - proof, - checkpointIndex - ); + fixturePrefix(checkpointIndex, merkleTreeAddress, messageId, proof); + + return + abi.encodePacked( + merkleTreeAddress, + checkpointIndex, + messageId, + proof, + checkpointIndex + ); } } @@ -224,15 +260,24 @@ contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest { mailbox.setRequiredHook(address(noopHook)); } + function fixturePrefix( + bytes32 root, + uint32 index, + bytes32 merkleTreeAddress + ) internal { + vm.serializeBytes32(prefixKey, "root", root); + vm.serializeUint(prefixKey, "signedIndex", uint256(index)); + vm.serializeBytes32(prefixKey, "merkleTree", merkleTreeAddress); + } + function metadataPrefix( bytes memory - ) internal override returns (bytes memory metadata, string memory prefix) { + ) internal override returns (bytes memory metadata) { (bytes32 root, uint32 index) = merkleTreeHook.latestCheckpoint(); bytes32 merkleTreeAddress = address(merkleTreeHook).addressToBytes32(); - prefix = "prefix"; - vm.serializeBytes32(prefix, "root", root); - vm.serializeUint(prefix, "signedIndex", uint256(index)); - prefix = vm.serializeBytes32(prefix, "merkleTree", merkleTreeAddress); - metadata = abi.encodePacked(merkleTreeAddress, root, index); + + fixturePrefix(root, index, merkleTreeAddress); + + return abi.encodePacked(merkleTreeAddress, root, index); } } diff --git a/solidity/test/isms/PolygonPosIsm.t.sol b/solidity/test/isms/PolygonPosIsm.t.sol new file mode 100644 index 0000000000..fd26afea29 --- /dev/null +++ b/solidity/test/isms/PolygonPosIsm.t.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MIT or Apache-2.0 +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {LibBit} from "../../contracts/libs/LibBit.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; +import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; +import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; +import {Message} from "../../contracts/libs/Message.sol"; +import {MessageUtils} from "./IsmTestUtils.sol"; +import {PolygonPosIsm} from "../../contracts/isms/hook/PolygonPosIsm.sol"; +import {PolygonPosHook} from "../../contracts/hooks/PolygonPosHook.sol"; +import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; + +import {NotCrossChainCall} from "@openzeppelin/contracts/crosschain/errors.sol"; + +interface IStateSender { + function counter() external view returns (uint256); +} + +interface FxChild { + function onStateReceive(uint256 stateId, bytes calldata data) external; +} + +contract PolygonPosIsmTest is Test { + using LibBit for uint256; + using TypeCasts for address; + using MessageUtils for bytes; + + uint256 internal mainnetFork; + uint256 internal polygonPosFork; + + address internal constant POLYGON_CROSSCHAIN_SYSTEM_ADDR = + 0x0000000000000000000000000000000000001001; + + address internal constant MUMBAI_FX_CHILD = + 0xCf73231F28B7331BBe3124B907840A94851f9f11; + address internal constant GOERLI_CHECKPOINT_MANAGER = + 0x2890bA17EfE978480615e330ecB65333b880928e; + address internal constant GOERLI_FX_ROOT = + 0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA; + + address internal constant MAINNET_FX_CHILD = + 0x8397259c983751DAf40400790063935a11afa28a; + address internal constant MAINNET_CHECKPOINT_MANAGER = + 0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287; + address internal constant MAINNET_FX_ROOT = + 0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2; + address internal constant MAINNET_STATE_SENDER = + 0x28e4F3a7f651294B9564800b2D01f35189A5bFbE; + + uint8 internal constant POLYGON_POS_VERSION = 0; + uint8 internal constant HYPERLANE_VERSION = 1; + + TestMailbox internal l1Mailbox; + PolygonPosIsm internal polygonPosISM; + PolygonPosHook internal polygonPosHook; + FxChild internal fxChild; + + TestRecipient internal testRecipient; + bytes internal testMessage = + abi.encodePacked("Hello from the other chain!"); + bytes internal testMetadata = + StandardHookMetadata.overrideRefundAddress(address(this)); + + bytes internal encodedMessage; + bytes32 internal messageId; + + uint32 internal constant MAINNET_DOMAIN = 1; + uint32 internal constant POLYGON_POS_DOMAIN = 137; + + event StateSynced( + uint256 indexed id, + address indexed contractAddress, + bytes data + ); + + event ReceivedMessage(bytes32 indexed messageId); + + function setUp() public { + // block numbers to fork from, chain data is cached to ../../forge-cache/ + mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 18_718_401); + polygonPosFork = vm.createFork(vm.rpcUrl("polygon"), 50_760_479); + + testRecipient = new TestRecipient(); + + encodedMessage = _encodeTestMessage(); + messageId = Message.id(encodedMessage); + } + + /////////////////////////////////////////////////////////////////// + /// SETUP /// + /////////////////////////////////////////////////////////////////// + + function deployPolygonPosHook() public { + vm.selectFork(mainnetFork); + + l1Mailbox = new TestMailbox(MAINNET_DOMAIN); + + polygonPosHook = new PolygonPosHook( + address(l1Mailbox), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(polygonPosISM)), + MAINNET_CHECKPOINT_MANAGER, + MAINNET_FX_ROOT + ); + + polygonPosHook.setFxChildTunnel(address(polygonPosISM)); + + vm.makePersistent(address(polygonPosHook)); + } + + function deployPolygonPosIsm() public { + vm.selectFork(polygonPosFork); + + fxChild = FxChild(MAINNET_FX_CHILD); + polygonPosISM = new PolygonPosIsm(MAINNET_FX_CHILD); + + vm.makePersistent(address(polygonPosISM)); + } + + function deployAll() public { + deployPolygonPosIsm(); + deployPolygonPosHook(); + + vm.selectFork(polygonPosFork); + + polygonPosISM.setAuthorizedHook( + TypeCasts.addressToBytes32(address(polygonPosHook)) + ); + } + + /////////////////////////////////////////////////////////////////// + /// FORK TESTS /// + /////////////////////////////////////////////////////////////////// + + /* ============ hook.quoteDispatch ============ */ + + function testFork_quoteDispatch() public { + deployAll(); + + vm.selectFork(mainnetFork); + + assertEq(polygonPosHook.quoteDispatch(testMetadata, encodedMessage), 0); + } + + /* ============ hook.postDispatch ============ */ + + function testFork_postDispatch() public { + deployAll(); + + vm.selectFork(mainnetFork); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + l1Mailbox.updateLatestDispatchedId(messageId); + + IStateSender stateSender = IStateSender(MAINNET_STATE_SENDER); + + vm.expectEmit(true, false, false, true); + emit StateSynced( + (stateSender.counter() + 1), + MAINNET_FX_CHILD, + abi.encode( + TypeCasts.addressToBytes32(address(polygonPosHook)), + TypeCasts.addressToBytes32(address(polygonPosISM)), + encodedHookData + ) + ); + polygonPosHook.postDispatch(testMetadata, encodedMessage); + } + + function testFork_postDispatch_RevertWhen_ChainIDNotSupported() public { + deployAll(); + + vm.selectFork(mainnetFork); + + bytes memory message = MessageUtils.formatMessage( + POLYGON_POS_VERSION, + uint32(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + 11, // wrong domain + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + + l1Mailbox.updateLatestDispatchedId(Message.id(message)); + vm.expectRevert( + "AbstractMessageIdAuthHook: invalid destination domain" + ); + polygonPosHook.postDispatch(testMetadata, message); + } + + function testFork_postDispatch_RevertWhen_TooMuchValue() public { + deployAll(); + + vm.selectFork(mainnetFork); + + // assign any value should revert + vm.deal(address(this), uint256(2 ** 255)); + bytes memory excessValueMetadata = StandardHookMetadata + .overrideMsgValue(uint256(2 ** 255)); + + l1Mailbox.updateLatestDispatchedId(messageId); + vm.expectRevert( + "AbstractMessageIdAuthHook: msgValue must be less than 2 ** 255" + ); + polygonPosHook.postDispatch(excessValueMetadata, encodedMessage); + } + + function testFork_postDispatch_RevertWhen_NotLastDispatchedMessage() + public + { + deployAll(); + + vm.selectFork(mainnetFork); + + vm.expectRevert( + "AbstractMessageIdAuthHook: message not latest dispatched" + ); + polygonPosHook.postDispatch(testMetadata, encodedMessage); + } + + /* ============ ISM.verifyMessageId ============ */ + + function testFork_verifyMessageId() public { + deployAll(); + + vm.selectFork(polygonPosFork); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + vm.startPrank(POLYGON_CROSSCHAIN_SYSTEM_ADDR); + + vm.expectEmit(true, false, false, false, address(polygonPosISM)); + emit ReceivedMessage(messageId); + // FIX: expect other events + + fxChild.onStateReceive( + 0, + abi.encode( + TypeCasts.addressToBytes32(address(polygonPosHook)), + TypeCasts.addressToBytes32(address(polygonPosISM)), + encodedHookData + ) + ); + + assertTrue(polygonPosISM.verifiedMessages(messageId).isBitSet(255)); + vm.stopPrank(); + } + + function testFork_verifyMessageId_RevertWhen_NotAuthorized() public { + deployAll(); + + vm.selectFork(polygonPosFork); + + // needs to be called by the fxchild on Polygon + vm.expectRevert(NotCrossChainCall.selector); + polygonPosISM.verifyMessageId(messageId); + + vm.startPrank(MAINNET_FX_CHILD); + + // needs to be called by the authorized hook contract on Ethereum + vm.expectRevert( + "AbstractMessageIdAuthorizedIsm: sender is not the hook" + ); + polygonPosISM.verifyMessageId(messageId); + } + + /* ============ ISM.verify ============ */ + + function testFork_verify() public { + deployAll(); + + vm.selectFork(polygonPosFork); + + orchestrateRelayMessage(messageId); + + bool verified = polygonPosISM.verify(new bytes(0), encodedMessage); + assertTrue(verified); + } + + // sending over invalid message + function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public { + deployAll(); + + orchestrateRelayMessage(messageId); + + bytes memory invalidMessage = MessageUtils.formatMessage( + HYPERLANE_VERSION, + uint8(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(this)), // wrong recipient + testMessage + ); + bool verified = polygonPosISM.verify(new bytes(0), invalidMessage); + assertFalse(verified); + } + + // invalid messageID in postDispatch + function testFork_verify_RevertWhen_InvalidPolygonPosMessageID() public { + deployAll(); + vm.selectFork(polygonPosFork); + + bytes memory invalidMessage = MessageUtils.formatMessage( + HYPERLANE_VERSION, + uint8(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + testMessage + ); + bytes32 _messageId = Message.id(invalidMessage); + orchestrateRelayMessage(_messageId); + + bool verified = polygonPosISM.verify(new bytes(0), encodedMessage); + assertFalse(verified); + } + + /* ============ helper functions ============ */ + + function _encodeTestMessage() internal view returns (bytes memory) { + return + MessageUtils.formatMessage( + HYPERLANE_VERSION, + uint32(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + } + + function orchestrateRelayMessage(bytes32 _messageId) internal { + vm.selectFork(polygonPosFork); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (_messageId) + ); + + vm.prank(POLYGON_CROSSCHAIN_SYSTEM_ADDR); + + fxChild.onStateReceive( + 0, + abi.encode( + TypeCasts.addressToBytes32(address(polygonPosHook)), + TypeCasts.addressToBytes32(address(polygonPosISM)), + encodedHookData + ) + ); + } +} diff --git a/solidity/test/token/HypERC20.t.sol b/solidity/test/token/HypERC20.t.sol index 7902e8116b..5be0bd5ed4 100644 --- a/solidity/test/token/HypERC20.t.sol +++ b/solidity/test/token/HypERC20.t.sol @@ -19,13 +19,17 @@ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transpa import {Mailbox} from "../../contracts/Mailbox.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; -import {ERC20Test} from "../../contracts/test/ERC20Test.sol"; +import {XERC20Test, FiatTokenTest, ERC20Test} from "../../contracts/test/ERC20Test.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol"; import {GasRouter} from "../../contracts/client/GasRouter.sol"; import {HypERC20} from "../../contracts/token/HypERC20.sol"; import {HypERC20Collateral} from "../../contracts/token/HypERC20Collateral.sol"; +import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol"; +import {IFiatToken} from "../../contracts/token/interfaces/IFiatToken.sol"; +import {HypXERC20Collateral} from "../../contracts/token/extensions/HypXERC20Collateral.sol"; +import {HypFiatTokenCollateral} from "../../contracts/token/extensions/HypFiatTokenCollateral.sol"; import {HypNative} from "../../contracts/token/HypNative.sol"; import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol"; import {TokenMessage} from "../../contracts/token/libs/TokenMessage.sol"; @@ -390,6 +394,108 @@ contract HypERC20CollateralTest is HypTokenTest { } } +contract HypXERC20CollateralTest is HypTokenTest { + using TypeCasts for address; + HypXERC20Collateral internal xerc20Collateral; + + function setUp() public override { + super.setUp(); + + primaryToken = new XERC20Test(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS); + + localToken = new HypXERC20Collateral( + address(primaryToken), + address(localMailbox) + ); + xerc20Collateral = HypXERC20Collateral(address(localToken)); + + xerc20Collateral.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + + primaryToken.transfer(address(localToken), 1000e18); + primaryToken.transfer(ALICE, 1000e18); + + _enrollRemoteTokenRouter(); + } + + function testRemoteTransfer() public { + uint256 balanceBefore = localToken.balanceOf(ALICE); + + vm.prank(ALICE); + primaryToken.approve(address(localToken), TRANSFER_AMT); + vm.expectCall( + address(primaryToken), + abi.encodeCall(IXERC20.burn, (ALICE, TRANSFER_AMT)) + ); + _performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0); + assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT); + } + + function testHandle() public { + vm.expectCall( + address(primaryToken), + abi.encodeCall(IXERC20.mint, (ALICE, TRANSFER_AMT)) + ); + _handleLocalTransfer(TRANSFER_AMT); + } +} + +contract HypFiatTokenCollateralTest is HypTokenTest { + using TypeCasts for address; + HypFiatTokenCollateral internal fiatTokenCollateral; + + function setUp() public override { + super.setUp(); + + primaryToken = new FiatTokenTest(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS); + + localToken = new HypFiatTokenCollateral( + address(primaryToken), + address(localMailbox) + ); + fiatTokenCollateral = HypFiatTokenCollateral(address(localToken)); + + fiatTokenCollateral.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + + primaryToken.transfer(address(localToken), 1000e18); + primaryToken.transfer(ALICE, 1000e18); + + _enrollRemoteTokenRouter(); + } + + function testRemoteTransfer() public { + uint256 balanceBefore = localToken.balanceOf(ALICE); + + vm.prank(ALICE); + primaryToken.approve(address(localToken), TRANSFER_AMT); + vm.expectCall( + address(primaryToken), + abi.encodeCall(IFiatToken.burn, (TRANSFER_AMT)) + ); + _performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0); + assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT); + } + + function testHandle() public { + bytes memory data = abi.encodeCall( + IFiatToken.mint, + (ALICE, TRANSFER_AMT) + ); + vm.mockCall(address(primaryToken), 0, data, abi.encode(false)); + vm.expectRevert("FiatToken mint failed"); + _handleLocalTransfer(TRANSFER_AMT); + vm.clearMockedCalls(); + + vm.expectCall(address(primaryToken), data); + _handleLocalTransfer(TRANSFER_AMT); + } +} + contract HypNativeTest is HypTokenTest { using TypeCasts for address; HypNative internal nativeToken; diff --git a/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol b/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol index 3dc4e75323..d71af8d566 100644 --- a/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol +++ b/solidity/test/token/HypERC20CollateralVaultDeposit.t.sol @@ -20,7 +20,7 @@ import {ERC4626Test} from "../../contracts/test/ERC4626/ERC4626Test.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {HypTokenTest} from "./HypERC20.t.sol"; -import {HypERC20CollateralVaultDeposit} from "../../contracts/token/HypERC20CollateralVaultDeposit.sol"; +import {HypERC20CollateralVaultDeposit} from "../../contracts/token/extensions/HypERC20CollateralVaultDeposit.sol"; import "../../contracts/test/ERC4626/ERC4626Test.sol"; contract HypERC20CollateralVaultDepositTest is HypTokenTest { diff --git a/typescript/ccip-server/CHANGELOG.md b/typescript/ccip-server/CHANGELOG.md index 1132173dab..561ec88454 100644 --- a/typescript/ccip-server/CHANGELOG.md +++ b/typescript/ccip-server/CHANGELOG.md @@ -1,5 +1,9 @@ # @hyperlane-xyz/ccip-server +## 3.11.1 + +## 3.11.0 + ## 3.10.0 ### Minor Changes diff --git a/typescript/ccip-server/package.json b/typescript/ccip-server/package.json index 109ee72d01..1fe18f6bcf 100644 --- a/typescript/ccip-server/package.json +++ b/typescript/ccip-server/package.json @@ -1,6 +1,6 @@ { "name": "@hyperlane-xyz/ccip-server", - "version": "3.10.0", + "version": "3.11.1", "description": "CCIP server", "typings": "dist/index.d.ts", "typedocMain": "src/index.ts", diff --git a/typescript/cli/CHANGELOG.md b/typescript/cli/CHANGELOG.md index 8e046ef21c..118f939439 100644 --- a/typescript/cli/CHANGELOG.md +++ b/typescript/cli/CHANGELOG.md @@ -1,5 +1,48 @@ # @hyperlane-xyz/cli +## 3.11.1 + +### Patch Changes + +- 78b77eecf: Fixes for CLI dry-runs +- Updated dependencies [c900da187] + - @hyperlane-xyz/sdk@3.11.1 + - @hyperlane-xyz/utils@3.11.1 + +## 3.11.0 + +### Minor Changes + +- f8b6ea467: Update the warp-route-deployment.yaml to a more sensible schema. This schema sets us up to allow multi-chain collateral deployments. Removes intermediary config objects by using zod instead. +- b6fdf2f7f: Implement XERC20 and FiatToken collateral warp routes +- aea79c686: Adds single-chain dry-run support for deploying warp routes & gas estimation for core and warp route dry-run deployments. +- 917266dce: Add --self-relay to CLI commands +- b63714ede: Convert all public hyperlane npm packages from CJS to pure ESM +- 450e8e0d5: Migrate fork util from CLI to SDK. Anvil IP & Port are now optionally passed into fork util by client. +- 3528b281e: Restructure CLI params around registries +- af2634207: Introduces `hyperlane hook read` and `hyperlane ism read` commands for deriving onchain Hook/ISM configs from an address on a given chain. + +### Patch Changes + +- 8246f14d6: Adds defaultDescription to yargs --key option. +- Updated dependencies [811ecfbba] +- Updated dependencies [f8b6ea467] +- Updated dependencies [d37cbab72] +- Updated dependencies [b6fdf2f7f] +- Updated dependencies [a86a8296b] +- Updated dependencies [2db77f177] +- Updated dependencies [3a08e31b6] +- Updated dependencies [917266dce] +- Updated dependencies [aab63d466] +- Updated dependencies [2e439423e] +- Updated dependencies [b63714ede] +- Updated dependencies [3528b281e] +- Updated dependencies [450e8e0d5] +- Updated dependencies [2b3f75836] +- Updated dependencies [af2634207] + - @hyperlane-xyz/sdk@3.11.0 + - @hyperlane-xyz/utils@3.11.0 + ## 3.10.0 ### Minor Changes diff --git a/typescript/cli/examples/warp-route-deployment.yaml b/typescript/cli/examples/warp-route-deployment.yaml index 2bad109404..d31f4c599a 100644 --- a/typescript/cli/examples/warp-route-deployment.yaml +++ b/typescript/cli/examples/warp-route-deployment.yaml @@ -5,10 +5,8 @@ # native # collateral # synthetic -# collateralUri -# syntheticUri -# fastCollateral -# fastSynthetic +# +# see comprehensive [list](https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/token/config.ts#L8) --- anvil1: type: native diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 13316ab08e..b5ce7a6f13 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -1,11 +1,11 @@ { "name": "@hyperlane-xyz/cli", - "version": "3.10.0", + "version": "3.11.1", "description": "A command-line utility for common Hyperlane operations", "dependencies": { "@hyperlane-xyz/registry": "^1.0.7", - "@hyperlane-xyz/sdk": "3.10.0", - "@hyperlane-xyz/utils": "3.10.0", + "@hyperlane-xyz/sdk": "3.11.1", + "@hyperlane-xyz/utils": "3.11.1", "@inquirer/prompts": "^3.0.0", "bignumber.js": "^9.1.1", "chalk": "^5.3.0", diff --git a/typescript/cli/src/commands/deploy.ts b/typescript/cli/src/commands/deploy.ts index 97bd1124d3..e9c8726530 100644 --- a/typescript/cli/src/commands/deploy.ts +++ b/typescript/cli/src/commands/deploy.ts @@ -6,7 +6,7 @@ import { } from '../context/types.js'; import { runKurtosisAgentDeploy } from '../deploy/agent.js'; import { runCoreDeploy } from '../deploy/core.js'; -import { evaluateIfDryRunFailure, verifyAnvil } from '../deploy/dry-run.js'; +import { evaluateIfDryRunFailure } from '../deploy/dry-run.js'; import { runWarpRouteDeploy } from '../deploy/warp.js'; import { log, logGray } from '../logger.js'; @@ -72,7 +72,7 @@ const coreCommand: CommandModuleWithWriteContext<{ targets: string; ism?: string; hook?: string; - 'dry-run': boolean; + 'dry-run': string; agent: string; }> = { command: 'core', @@ -90,8 +90,6 @@ const coreCommand: CommandModuleWithWriteContext<{ ); logGray('------------------------------------------------'); - if (dryRun) await verifyAnvil(); - try { const chains = targets?.split(',').map((r: string) => r.trim()); await runCoreDeploy({ @@ -114,7 +112,7 @@ const coreCommand: CommandModuleWithWriteContext<{ */ const warpCommand: CommandModuleWithWriteContext<{ config: string; - 'dry-run': boolean; + 'dry-run': string; }> = { command: 'warp', describe: 'Deploy Warp Route contracts', @@ -126,8 +124,6 @@ const warpCommand: CommandModuleWithWriteContext<{ logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`); logGray('------------------------------------------------'); - if (dryRun) await verifyAnvil(); - try { await runWarpRouteDeploy({ context, diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 6e33a82db3..465abb09e0 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -1,3 +1,4 @@ +import os from 'os'; import { Options } from 'yargs'; import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry'; @@ -29,7 +30,7 @@ export const registryUriCommandOption: Options = { export const overrideRegistryUriCommandOption: Options = { type: 'string', description: 'Path to a local registry to override the default registry', - default: './', + default: `${os.homedir()}/.hyperlane`, }; export const skipConfirmationOption: Options = { @@ -45,6 +46,7 @@ export const keyCommandOption: Options = { Dry-run: An address to simulate transaction signing on a forked network`, alias: 'k', default: ENV.HYP_KEY, + defaultDescription: 'process.env.HYP_KEY', }; /* Command-specific options */ @@ -122,7 +124,7 @@ export const dryRunOption: Options = { type: 'string', description: 'Chain name to fork and simulate deployment. Please ensure an anvil node instance is running during execution via `anvil`.', - alias: ['d'], + alias: 'd', }; export const chainCommandOption: Options = { diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index c47b8f1c7c..232205dc04 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -1,10 +1,12 @@ import { confirm, input } from '@inquirer/prompts'; +import { ethers } from 'ethers'; import { ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; +import { detectAndConfirmOrPrompt } from '../utils/chains.js'; import { readYamlOrJson } from '../utils/files.js'; export function readChainConfigs(filePath: string) { @@ -38,22 +40,52 @@ export async function createChainConfig({ context: CommandContext; }) { logBlue('Creating a new chain config'); - const name = await input({ - message: 'Enter chain name (one word, lower case)', - }); - const chainId = await input({ message: 'Enter chain id (number)' }); - const domainId = chainId; - const rpcUrl = await input({ message: 'Enter http or https rpc url' }); + + const rpcUrl = await detectAndConfirmOrPrompt( + async () => { + await new ethers.providers.JsonRpcProvider().getNetwork(); + return ethers.providers.JsonRpcProvider.defaultUrl(); + }, + 'rpc url', + 'Enter http or https', + ); + const provider = new ethers.providers.JsonRpcProvider(rpcUrl); + + const name = await detectAndConfirmOrPrompt( + async () => { + const clientName = await provider.send('web3_clientVersion', []); + const port = rpcUrl.split(':').slice(-1); + const client = clientName.split('/')[0]; + return `${client}${port}`; + }, + 'chain name', + 'Enter (one word, lower case)', + ); + + const chainId = parseInt( + await detectAndConfirmOrPrompt( + async () => { + const network = await provider.getNetwork(); + return network.chainId.toString(); + }, + 'chain id', + 'Enter a (number)', + ), + 10, + ); + const metadata: ChainMetadata = { name, - chainId: parseInt(chainId, 10), - domainId: parseInt(domainId, 10), + chainId, + domainId: chainId, protocol: ProtocolType.Ethereum, rpcUrls: [{ http: rpcUrl }], }; + const wantAdvancedConfig = await confirm({ + default: false, message: - 'Do you want to set block or gas properties for this chain config?(optional)', + 'Do you want to set block or gas properties for this chain config?', }); if (wantAdvancedConfig) { const wantBlockConfig = await confirm({ diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 2410915d0d..19f1a1fbde 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -2,12 +2,14 @@ import { confirm, input } from '@inquirer/prompts'; import { ethers } from 'ethers'; import { + ChainMetadata, TokenType, WarpCoreConfig, WarpCoreConfigSchema, WarpRouteDeployConfig, WarpRouteDeployConfigSchema, } from '@hyperlane-xyz/sdk'; +import { objFilter } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, logBlue, logGreen } from '../logger.js'; @@ -66,8 +68,12 @@ export async function createWarpRouteDeployConfig({ ? ethers.constants.AddressZero : await input({ message: addressMessage }); - const syntheticChains = await runMultiChainSelectionStep( + const metadataWithoutBase = objFilter( context.chainMetadata, + (chain, _): _ is ChainMetadata => chain !== baseChain, + ); + const syntheticChains = await runMultiChainSelectionStep( + metadataWithoutBase, 'Select chains to which the base token will be connected', ); diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index e3984408cb..71fffa5c9a 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -2,9 +2,11 @@ import { ethers } from 'ethers'; import { IRegistry } from '@hyperlane-xyz/registry'; import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk'; +import { isNullish } from '@hyperlane-xyz/utils'; import { isSignCommand } from '../commands/signCommands.js'; -import { forkNetworkToMultiProvider } from '../deploy/dry-run.js'; +import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; +import { logBlue } from '../logger.js'; import { MergedRegistry } from '../registry/MergedRegistry.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { getImpersonatedSigner, getSigner } from '../utils/keys.js'; @@ -16,7 +18,7 @@ import { } from './types.js'; export async function contextMiddleware(argv: Record) { - const isDryRun = !!argv.dryRun; + const isDryRun = !isNullish(argv.dryRun); const requiresKey = isSignCommand(argv); const settings: ContextSettings = { registryUri: argv.registry, @@ -79,6 +81,9 @@ export async function getDryRunContext( ); } + logBlue(`Dry-running against chain: ${chain}`); + await verifyAnvil(); + const multiProvider = await getMultiProvider(registry); await forkNetworkToMultiProvider(multiProvider, chain); const { key: impersonatedKey, signer: impersonatedSigner } = diff --git a/typescript/cli/src/deploy/dry-run.ts b/typescript/cli/src/deploy/dry-run.ts index 5ece8aaf3e..11cbe379bf 100644 --- a/typescript/cli/src/deploy/dry-run.ts +++ b/typescript/cli/src/deploy/dry-run.ts @@ -50,7 +50,7 @@ export async function verifyAnvil() { * @param error the thrown error * @param dryRun the chain name to execute the dry-run on */ -export function evaluateIfDryRunFailure(error: any, dryRun: boolean) { +export function evaluateIfDryRunFailure(error: any, dryRun: string) { if (dryRun && error.message.includes('call revert exception')) warnYellow( '⛔️ [dry-run] The current RPC may not support forking. Please consider using a different RPC provider.', diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index ba6207a550..a4a92c465a 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -227,7 +227,7 @@ async function executeDeploy(params: DeployParams) { logGreen('✅ Hyp token deployments complete'); - log('Writing deployment artifacts'); + if (!isDryRun) log('Writing deployment artifacts'); const warpCoreConfig = getWarpCoreConfig(params, deployedContracts); await registry.addWarpRoute(warpCoreConfig); log(JSON.stringify(warpCoreConfig, null, 2)); diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index 161c37cb24..dcd3aa6fd7 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -138,7 +138,7 @@ async function executeDelivery({ sender: senderAddress, }); if (errors) { - logRed('Unable to validate transfer', errors); + logRed('Error validating transfer', JSON.stringify(errors)); throw new Error('Error validating transfer'); } diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts index e3eeecfaeb..470f0a20da 100644 --- a/typescript/cli/src/utils/chains.ts +++ b/typescript/cli/src/utils/chains.ts @@ -1,4 +1,4 @@ -import { Separator, checkbox } from '@inquirer/prompts'; +import { Separator, checkbox, confirm, input } from '@inquirer/prompts'; import select from '@inquirer/select'; import chalk from 'chalk'; @@ -18,7 +18,7 @@ export async function runSingleChainSelectionStep( const chain = (await select({ message, choices, - pageSize: 20, + pageSize: 30, })) as string; handleNewChain([chain]); return chain; @@ -35,7 +35,7 @@ export async function runMultiChainSelectionStep( const chains = (await checkbox({ message, choices, - pageSize: 20, + pageSize: 30, })) as string[]; handleNewChain(chains); if (requireMultiple && chains?.length < 2) { @@ -73,3 +73,22 @@ function handleNewChain(chainNames: string[]) { process.exit(0); } } + +export async function detectAndConfirmOrPrompt( + detect: () => Promise, + label: string, + prompt: string, +): Promise { + let detectedValue: string | undefined; + try { + detectedValue = await detect(); + const confirmed = await confirm({ + message: `Detected ${label} as ${detectedValue}, is this correct?`, + }); + if (confirmed) { + return detectedValue; + } + // eslint-disable-next-line no-empty + } catch (e) {} + return input({ message: `${prompt} ${label}`, default: detectedValue }); +} diff --git a/typescript/cli/src/version.ts b/typescript/cli/src/version.ts index 51b4d91b21..68e87af9ac 100644 --- a/typescript/cli/src/version.ts +++ b/typescript/cli/src/version.ts @@ -1 +1 @@ -export const VERSION = '3.10.0'; +export const VERSION = '3.11.1'; diff --git a/typescript/helloworld/CHANGELOG.md b/typescript/helloworld/CHANGELOG.md index 8c5c68facc..fec1510878 100644 --- a/typescript/helloworld/CHANGELOG.md +++ b/typescript/helloworld/CHANGELOG.md @@ -1,5 +1,38 @@ # @hyperlane-xyz/helloworld +## 3.11.1 + +### Patch Changes + +- Updated dependencies [c900da187] + - @hyperlane-xyz/sdk@3.11.1 + - @hyperlane-xyz/core@3.11.1 + +## 3.11.0 + +### Minor Changes + +- b63714ede: Convert all public hyperlane npm packages from CJS to pure ESM + +### Patch Changes + +- Updated dependencies [811ecfbba] +- Updated dependencies [f8b6ea467] +- Updated dependencies [d37cbab72] +- Updated dependencies [b6fdf2f7f] +- Updated dependencies [a86a8296b] +- Updated dependencies [2db77f177] +- Updated dependencies [3a08e31b6] +- Updated dependencies [917266dce] +- Updated dependencies [aab63d466] +- Updated dependencies [2e439423e] +- Updated dependencies [b63714ede] +- Updated dependencies [3528b281e] +- Updated dependencies [450e8e0d5] +- Updated dependencies [af2634207] + - @hyperlane-xyz/sdk@3.11.0 + - @hyperlane-xyz/core@3.11.0 + ## 3.10.0 ### Minor Changes diff --git a/typescript/helloworld/package.json b/typescript/helloworld/package.json index 3834296e41..edd483b2f7 100644 --- a/typescript/helloworld/package.json +++ b/typescript/helloworld/package.json @@ -1,11 +1,11 @@ { "name": "@hyperlane-xyz/helloworld", "description": "A basic skeleton of an Hyperlane app", - "version": "3.10.0", + "version": "3.11.1", "dependencies": { - "@hyperlane-xyz/core": "3.10.0", + "@hyperlane-xyz/core": "3.11.1", "@hyperlane-xyz/registry": "^1.0.7", - "@hyperlane-xyz/sdk": "3.10.0", + "@hyperlane-xyz/sdk": "3.11.1", "@openzeppelin/contracts-upgradeable": "^4.9.3", "ethers": "^5.7.2" }, diff --git a/typescript/infra/CHANGELOG.md b/typescript/infra/CHANGELOG.md index 7959c91cde..4c249b07d8 100644 --- a/typescript/infra/CHANGELOG.md +++ b/typescript/infra/CHANGELOG.md @@ -1,5 +1,42 @@ # @hyperlane-xyz/infra +## 3.11.1 + +### Patch Changes + +- Updated dependencies [c900da187] + - @hyperlane-xyz/sdk@3.11.1 + - @hyperlane-xyz/helloworld@3.11.1 + - @hyperlane-xyz/utils@3.11.1 + +## 3.11.0 + +### Minor Changes + +- af2634207: Moved Hook/ISM reading into CLI. + +### Patch Changes + +- a86a8296b: Removes Gnosis safe util from infra in favor of SDK +- Updated dependencies [811ecfbba] +- Updated dependencies [f8b6ea467] +- Updated dependencies [d37cbab72] +- Updated dependencies [b6fdf2f7f] +- Updated dependencies [a86a8296b] +- Updated dependencies [2db77f177] +- Updated dependencies [3a08e31b6] +- Updated dependencies [917266dce] +- Updated dependencies [aab63d466] +- Updated dependencies [2e439423e] +- Updated dependencies [b63714ede] +- Updated dependencies [3528b281e] +- Updated dependencies [450e8e0d5] +- Updated dependencies [2b3f75836] +- Updated dependencies [af2634207] + - @hyperlane-xyz/sdk@3.11.0 + - @hyperlane-xyz/helloworld@3.11.0 + - @hyperlane-xyz/utils@3.11.0 + ## 3.10.0 ### Minor Changes diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 8d1c9e2102..b65dfbc774 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -76,7 +76,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig = { ethereum: true, // At the moment, we only relay between Neutron and Manta Pacific on the neutron context. neutron: false, - mantapacific: false, + mantapacific: true, mode: true, moonbeam: true, optimism: true, @@ -202,7 +202,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: 'a2d6af6-20240422-164135', + tag: '3012392-20240507-130024', }, gasPaymentEnforcement: gasPaymentEnforcement, metricAppContexts, @@ -233,7 +233,7 @@ const releaseCandidate: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: 'a2d6af6-20240422-164135', + tag: '3012392-20240507-130024', }, // We're temporarily (ab)using the RC relayer as a way to increase // message throughput. diff --git a/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml b/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml index a9cc54da90..f0870da0a9 100644 --- a/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml +++ b/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml @@ -32,12 +32,9 @@ spec: * to replace the correct value in the created secret. */}} {{- range .Values.hyperlane.chains }} - {{- if or (eq $.Values.hyperlane.connectionType "quorum") (eq $.Values.hyperlane.connectionType "fallback") }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} - {{- else }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }} {{- end }} - {{- end }} {{- if .Values.hyperlane.aws }} AWS_ACCESS_KEY_ID: {{ print "'{{ .aws_access_key_id | toString }}'" }} AWS_SECRET_ACCESS_KEY: {{ print "'{{ .aws_secret_access_key | toString }}'" }} @@ -51,16 +48,13 @@ spec: * and associate it with the secret key networkname_rpc. */}} {{- range .Values.hyperlane.chains }} - {{- if or (eq $.Values.hyperlane.connectionType "quorum") (eq $.Values.hyperlane.connectionType "fallback") }} - secretKey: {{ printf "%s_rpcs" . }} remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }} - {{- else }} - secretKey: {{ printf "%s_rpc" . }} remoteRef: key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }} {{- end }} - {{- end }} {{- if .Values.hyperlane.aws }} - secretKey: aws_access_key_id remoteRef: diff --git a/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml b/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml index 6e939c5df9..de6577bfaa 100644 --- a/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml +++ b/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml @@ -28,12 +28,9 @@ spec: * to replace the correct value in the created secret. */}} {{- range .Values.hyperlane.chains }} - {{- if eq $.Values.hyperlane.connectionType "quorum" }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} - {{- else }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }} {{- end }} - {{- end }} data: - secretKey: deployer_key remoteRef: @@ -43,13 +40,10 @@ spec: * and associate it with the secret key networkname_rpc. */}} {{- range .Values.hyperlane.chains }} - {{- if eq $.Values.hyperlane.connectionType "quorum" }} - secretKey: {{ printf "%s_rpcs" . }} remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }} - {{- else }} - secretKey: {{ printf "%s_rpc" . }} remoteRef: key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }} {{- end }} - {{- end }} diff --git a/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml b/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml index 1ac51df9e4..a8d44b48cf 100644 --- a/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml +++ b/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml @@ -28,12 +28,9 @@ spec: * to replace the correct value in the created secret. */}} {{- range .Values.hyperlane.chains }} - {{- if eq $.Values.hyperlane.connectionType "quorum" }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} - {{- else }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }} {{- end }} - {{- end }} data: - secretKey: deployer_key remoteRef: @@ -43,13 +40,10 @@ spec: * and associate it with the secret key networkname_rpc. */}} {{- range .Values.hyperlane.chains }} - {{- if eq $.Values.hyperlane.connectionType "quorum" }} - secretKey: {{ printf "%s_rpcs" . }} remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }} - {{- else }} - secretKey: {{ printf "%s_rpc" . }} remoteRef: key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }} {{- end }} - {{- end }} diff --git a/typescript/infra/package.json b/typescript/infra/package.json index cb062f9e76..a000f25e14 100644 --- a/typescript/infra/package.json +++ b/typescript/infra/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/infra", "description": "Infrastructure utilities for the Hyperlane Network", - "version": "3.10.0", + "version": "3.11.1", "dependencies": { "@arbitrum/sdk": "^3.0.0", "@aws-sdk/client-iam": "^3.74.0", @@ -12,13 +12,11 @@ "@ethersproject/experimental": "^5.7.0", "@ethersproject/hardware-wallets": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@hyperlane-xyz/helloworld": "3.10.0", + "@hyperlane-xyz/helloworld": "3.11.1", "@hyperlane-xyz/registry": "^1.0.7", - "@hyperlane-xyz/sdk": "3.10.0", - "@hyperlane-xyz/utils": "3.10.0", + "@hyperlane-xyz/sdk": "3.11.1", + "@hyperlane-xyz/utils": "3.11.1", "@nomiclabs/hardhat-etherscan": "^3.0.3", - "@safe-global/api-kit": "^1.3.0", - "@safe-global/protocol-kit": "^1.2.0", "@solana/web3.js": "^1.78.0", "asn1.js": "5.4.1", "aws-kms-ethers-signer": "^0.1.3", diff --git a/typescript/infra/scripts/safe-delegate.ts b/typescript/infra/scripts/safe-delegate.ts index f067c2db8c..f39b94ecef 100644 --- a/typescript/infra/scripts/safe-delegate.ts +++ b/typescript/infra/scripts/safe-delegate.ts @@ -4,8 +4,10 @@ import { LedgerSigner } from '@ethersproject/hardware-wallets'; import '@ethersproject/hardware-wallets/thirdparty'; import { AddSafeDelegateProps } from '@safe-global/api-kit'; +// @ts-ignore +import { getSafeDelegates, getSafeService } from '@hyperlane-xyz/sdk'; + import { getChains } from '../config/registry.js'; -import { getSafeDelegates, getSafeService } from '../src/utils/safe.js'; import { getArgs as getRootArgs } from './agent-utils.js'; import { getEnvironmentConfig } from './core-utils.js'; diff --git a/typescript/infra/src/agents/index.ts b/typescript/infra/src/agents/index.ts index eb8a680294..41eabefae3 100644 --- a/typescript/infra/src/agents/index.ts +++ b/typescript/infra/src/agents/index.ts @@ -133,6 +133,7 @@ export abstract class AgentHelmManager { rpcConsensusType: this.rpcConsensusType(chain), protocol: metadata.protocol, blocks: { reorgPeriod }, + maxBatchSize: 4, }; }), }, diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts index 98444af868..1da898ab13 100644 --- a/typescript/infra/src/govern/HyperlaneAppGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts @@ -12,6 +12,8 @@ import { OwnableConfig, OwnerViolation, } from '@hyperlane-xyz/sdk'; +// @ts-ignore +import { canProposeSafeTransactions } from '@hyperlane-xyz/sdk'; import { Address, CallData, @@ -20,8 +22,6 @@ import { objMap, } from '@hyperlane-xyz/utils'; -import { canProposeSafeTransactions } from '../utils/safe.js'; - import { ManualMultiSend, MultiSend, diff --git a/typescript/infra/src/govern/multisend.ts b/typescript/infra/src/govern/multisend.ts index 56929e902d..348762cfaa 100644 --- a/typescript/infra/src/govern/multisend.ts +++ b/typescript/infra/src/govern/multisend.ts @@ -1,8 +1,8 @@ import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk'; +// @ts-ignore +import { getSafe, getSafeService } from '@hyperlane-xyz/sdk'; import { CallData } from '@hyperlane-xyz/utils'; -import { getSafe, getSafeService } from '../utils/safe.js'; - export abstract class MultiSend { abstract sendTransactions(calls: CallData[]): Promise; } diff --git a/typescript/sdk/CHANGELOG.md b/typescript/sdk/CHANGELOG.md index 5a24d45756..f87c403051 100644 --- a/typescript/sdk/CHANGELOG.md +++ b/typescript/sdk/CHANGELOG.md @@ -1,5 +1,43 @@ # @hyperlane-xyz/sdk +## 3.11.1 + +### Patch Changes + +- c900da187: Workaround TS bug in Safe protocol-lib + - @hyperlane-xyz/core@3.11.1 + - @hyperlane-xyz/utils@3.11.1 + +## 3.11.0 + +### Minor Changes + +- 811ecfbba: Add EvmCoreReader, minor updates. +- f8b6ea467: Update the warp-route-deployment.yaml to a more sensible schema. This schema sets us up to allow multi-chain collateral deployments. Removes intermediary config objects by using zod instead. +- d37cbab72: Adds modular transaction submission support for SDK clients, e.g. CLI. +- b6fdf2f7f: Implement XERC20 and FiatToken collateral warp routes +- 2db77f177: Added RPC `concurrency` property to `ChainMetadata`. + Added `CrudModule` abstraction and related types. + Removed `Fuel` ProtocolType. +- 3a08e31b6: Add EvmERC20WarpRouterReader to derive WarpConfig from TokenRouter address +- 917266dce: Add --self-relay to CLI commands +- aab63d466: Adding ICA for governance +- b63714ede: Convert all public hyperlane npm packages from CJS to pure ESM +- 3528b281e: Remove consts such as chainMetadata from SDK +- 450e8e0d5: Migrate fork util from CLI to SDK. Anvil IP & Port are now optionally passed into fork util by client. +- af2634207: Moved Hook/ISM config stringify into a general object stringify utility. + +### Patch Changes + +- a86a8296b: Removes Gnosis safe util from infra in favor of SDK +- 2e439423e: Allow gasLimit overrides in the SDK/CLI for deploy txs +- Updated dependencies [b6fdf2f7f] +- Updated dependencies [b63714ede] +- Updated dependencies [2b3f75836] +- Updated dependencies [af2634207] + - @hyperlane-xyz/core@3.11.0 + - @hyperlane-xyz/utils@3.11.0 + ## 3.10.0 ### Minor Changes diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 09345a66d2..19b2b4b22c 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -1,13 +1,15 @@ { "name": "@hyperlane-xyz/sdk", "description": "The official SDK for the Hyperlane Network", - "version": "3.10.0", + "version": "3.11.1", "dependencies": { "@aws-sdk/client-s3": "^3.74.0", "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/stargate": "^0.31.3", - "@hyperlane-xyz/core": "3.10.0", - "@hyperlane-xyz/utils": "3.10.0", + "@hyperlane-xyz/core": "3.11.1", + "@hyperlane-xyz/utils": "3.11.1", + "@safe-global/api-kit": "1.3.0", + "@safe-global/protocol-kit": "1.3.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.0", "@types/coingecko-api": "^1.0.10", @@ -65,7 +67,8 @@ ], "license": "Apache-2.0", "scripts": { - "build": "tsc", + "build": "tsc && yarn copy-js", + "copy-js": "cp ./src/utils/*.js ./dist/utils", "dev": "tsc --watch", "check": "tsc --noEmit", "clean": "rm -rf ./dist ./cache", diff --git a/typescript/sdk/src/consts/crud.ts b/typescript/sdk/src/consts/concurrency.ts similarity index 100% rename from typescript/sdk/src/consts/crud.ts rename to typescript/sdk/src/consts/concurrency.ts diff --git a/typescript/sdk/src/core/AbstractHyperlaneModule.ts b/typescript/sdk/src/core/AbstractHyperlaneModule.ts new file mode 100644 index 0000000000..a5159ac851 --- /dev/null +++ b/typescript/sdk/src/core/AbstractHyperlaneModule.ts @@ -0,0 +1,45 @@ +import { Logger } from 'pino'; + +import { Annotated, ProtocolType } from '@hyperlane-xyz/utils'; + +import { ProtocolTypedTransaction } from '../providers/ProviderType.js'; +import { ChainNameOrId } from '../types.js'; + +export type HyperlaneModuleArgs< + TConfig, + TAddressMap extends Record, +> = { + addresses: TAddressMap; + chain: ChainNameOrId; + config: TConfig; +}; + +export abstract class HyperlaneModule< + TProtocol extends ProtocolType, + TConfig, + TAddressMap extends Record, +> { + protected abstract readonly logger: Logger; + + protected constructor( + protected readonly args: HyperlaneModuleArgs, + ) {} + + public serialize(): TAddressMap { + return this.args.addresses; + } + + public abstract read(): Promise; + public abstract update( + config: TConfig, + ): Promise[]>>; + + // /* + // Types and static methods can be challenging. Ensure each implementation includes a static create function. + // Currently, include TConfig to maintain the structure for ISM/Hook configurations. + // If found to be unnecessary, we may consider revisiting and potentially removing these config requirements later. + // */ + // public static create(_config: TConfig): Promise { + // throw new Error('not implemented'); + // } +} diff --git a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts index 90b3cfee43..41fd03add6 100644 --- a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts +++ b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts @@ -9,18 +9,18 @@ import { TestChainName, testChains } from '../consts/testChains.js'; import { HyperlaneContractsMap } from '../contracts/types.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { HookConfig } from '../hook/types.js'; +import { DerivedIsmConfigWithAddress } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; -import { DerivedIsmConfigWithAddress } from '../ism/read.js'; import { AggregationIsmConfig, IsmType } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { testCoreConfig } from '../test/testUtils.js'; import { ChainMap } from '../types.js'; +import { EvmCoreReader } from './EvmCoreReader.js'; import { HyperlaneCore } from './HyperlaneCore.js'; import { HyperlaneCoreChecker } from './HyperlaneCoreChecker.js'; import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer.js'; import { CoreFactories } from './contracts.js'; -import { EvmCoreReader } from './read.js'; import { CoreConfig } from './types.js'; describe('core', async () => { diff --git a/typescript/sdk/src/core/read.ts b/typescript/sdk/src/core/EvmCoreReader.ts similarity index 91% rename from typescript/sdk/src/core/read.ts rename to typescript/sdk/src/core/EvmCoreReader.ts index 85873e76df..c7edc135ee 100644 --- a/typescript/sdk/src/core/read.ts +++ b/typescript/sdk/src/core/EvmCoreReader.ts @@ -3,9 +3,9 @@ import { providers } from 'ethers'; import { Mailbox__factory } from '@hyperlane-xyz/core'; import { Address, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; -import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js'; -import { EvmHookReader } from '../hook/read.js'; -import { EvmIsmReader } from '../ism/read.js'; +import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; +import { EvmHookReader } from '../hook/EvmHookReader.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainNameOrId } from '../types.js'; diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index 42d88863b4..f3cb21877c 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -19,7 +19,10 @@ import { HyperlaneApp } from '../app/HyperlaneApp.js'; import { appFromAddressesMapHelper } from '../contracts/contracts.js'; import { HyperlaneAddressesMap } from '../contracts/types.js'; import { OwnableConfig } from '../deploy/types.js'; -import { DerivedIsmConfigWithAddress, EvmIsmReader } from '../ism/read.js'; +import { + DerivedIsmConfigWithAddress, + EvmIsmReader, +} from '../ism/EvmIsmReader.js'; import { IsmType, ModuleType, ismTypeToModuleType } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { RouterConfig } from '../router/types.js'; diff --git a/typescript/sdk/src/crud/AbstractCrudModule.ts b/typescript/sdk/src/crud/AbstractCrudModule.ts deleted file mode 100644 index 255258449f..0000000000 --- a/typescript/sdk/src/crud/AbstractCrudModule.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Logger } from 'pino'; - -import { Address, Annotated, ProtocolType } from '@hyperlane-xyz/utils'; - -import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; -import { - ProtocolTypedProvider, - ProtocolTypedTransaction, -} from '../providers/ProviderType.js'; -import { ChainNameOrId } from '../types.js'; - -export type CrudModuleArgs< - TProtocol extends ProtocolType, - TConfig, - TAddressMap extends Record, -> = { - addresses: TAddressMap; - chain: ChainNameOrId; - chainMetadataManager: ChainMetadataManager; - config: TConfig; - provider: ProtocolTypedProvider['provider']; -}; - -export abstract class CrudModule< - TProtocol extends ProtocolType, - TConfig, - TAddressMap extends Record, -> { - protected abstract readonly logger: Logger; - - protected constructor( - protected readonly args: CrudModuleArgs, - ) {} - - public serialize(): TAddressMap { - return this.args.addresses; - } - - public abstract read(address: Address): Promise; - public abstract update( - config: TConfig, - ): Promise[]>>; - - // /* - // Types and static methods can be challenging. Ensure each implementation includes a static create function. - // Currently, include TConfig to maintain the structure for ISM/Hook configurations. - // If found to be unnecessary, we may consider revisiting and potentially removing these config requirements later. - // */ - // public static create< - // TConfig extends CrudConfig, - // TProtocol extends ProtocolType, - // TAddress extends Record, - // TModule extends CrudModule, - // >(_config: TConfig): Promise { - // throw new Error('not implemented'); - // } -} diff --git a/typescript/sdk/src/crud/EvmIsmModule.ts b/typescript/sdk/src/crud/EvmIsmModule.ts deleted file mode 100644 index 820c4bc84c..0000000000 --- a/typescript/sdk/src/crud/EvmIsmModule.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; - -import { HyperlaneAddresses } from '../contracts/types.js'; -import { ProxyFactoryFactories } from '../deploy/contracts.js'; -import { EvmIsmReader } from '../ism/read.js'; -import { IsmConfig } from '../ism/types.js'; -import { MultiProvider } from '../providers/MultiProvider.js'; -import { EthersV5Transaction } from '../providers/ProviderType.js'; - -import { CrudModule, CrudModuleArgs } from './AbstractCrudModule.js'; - -// WIP example implementation of EvmIsmModule -export class EvmIsmModule extends CrudModule< - ProtocolType.Ethereum, - IsmConfig, - HyperlaneAddresses -> { - protected logger = rootLogger.child({ module: 'EvmIsmModule' }); - protected reader: EvmIsmReader; - - protected constructor( - protected readonly multiProvider: MultiProvider, - args: Omit< - CrudModuleArgs< - ProtocolType.Ethereum, - IsmConfig, - HyperlaneAddresses - >, - 'provider' - >, - ) { - super({ - ...args, - provider: multiProvider.getProvider(args.chain), - }); - - this.reader = new EvmIsmReader(multiProvider, args.chain); - } - - public async read(address: Address): Promise { - return await this.reader.deriveIsmConfig(address); - } - - public async update(_config: IsmConfig): Promise { - throw new Error('Method not implemented.'); - } - - // manually write static create function - public static create(_config: IsmConfig): Promise { - throw new Error('not implemented'); - } -} diff --git a/typescript/sdk/src/crud/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts similarity index 54% rename from typescript/sdk/src/crud/EvmHookModule.ts rename to typescript/sdk/src/hook/EvmHookModule.ts index 727135090e..c6e01bc87c 100644 --- a/typescript/sdk/src/crud/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -1,44 +1,45 @@ import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; import { HyperlaneAddresses } from '../contracts/types.js'; -import { HookFactories } from '../hook/contracts.js'; -import { EvmHookReader } from '../hook/read.js'; -import { HookConfig } from '../hook/types.js'; +import { + HyperlaneModule, + HyperlaneModuleArgs, +} from '../core/AbstractHyperlaneModule.js'; +import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { EthersV5Transaction } from '../providers/ProviderType.js'; -import { CrudModule, CrudModuleArgs } from './AbstractCrudModule.js'; +import { EvmHookReader } from './EvmHookReader.js'; +import { HookFactories } from './contracts.js'; +import { HookConfig } from './types.js'; // WIP example implementation of EvmHookModule -export class EvmHookModule extends CrudModule< +export class EvmHookModule extends HyperlaneModule< ProtocolType.Ethereum, HookConfig, - HyperlaneAddresses + HyperlaneAddresses & { + deployedHook: Address; + } > { protected logger = rootLogger.child({ module: 'EvmHookModule' }); protected reader: EvmHookReader; protected constructor( protected readonly multiProvider: MultiProvider, - args: Omit< - CrudModuleArgs< - ProtocolType.Ethereum, - HookConfig, - HyperlaneAddresses - >, - 'provider' + protected readonly deployer: HyperlaneDeployer, + args: HyperlaneModuleArgs< + HookConfig, + HyperlaneAddresses & { + deployedHook: Address; + } >, ) { - super({ - ...args, - provider: multiProvider.getProvider(args.chain), - }); - + super(args); this.reader = new EvmHookReader(multiProvider, args.chain); } - public async read(address: Address): Promise { - return await this.reader.deriveHookConfig(address); + public async read(): Promise { + return await this.reader.deriveHookConfig(this.args.addresses.deployedHook); } public async update(_config: HookConfig): Promise { diff --git a/typescript/sdk/src/hook/read.test.ts b/typescript/sdk/src/hook/EvmHookReader.test.ts similarity index 99% rename from typescript/sdk/src/hook/read.test.ts rename to typescript/sdk/src/hook/EvmHookReader.test.ts index dd98039676..77af11b990 100644 --- a/typescript/sdk/src/hook/read.test.ts +++ b/typescript/sdk/src/hook/EvmHookReader.test.ts @@ -19,7 +19,7 @@ import { WithAddress } from '@hyperlane-xyz/utils'; import { TestChainName, test1 } from '../consts/testChains.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { EvmHookReader } from './read.js'; +import { EvmHookReader } from './EvmHookReader.js'; import { HookType, MerkleTreeHookConfig, diff --git a/typescript/sdk/src/hook/read.ts b/typescript/sdk/src/hook/EvmHookReader.ts similarity index 98% rename from typescript/sdk/src/hook/read.ts rename to typescript/sdk/src/hook/EvmHookReader.ts index d3c6df4bfd..928f39b6d2 100644 --- a/typescript/sdk/src/hook/read.ts +++ b/typescript/sdk/src/hook/EvmHookReader.ts @@ -1,4 +1,3 @@ -import { assert } from 'console'; import { ethers, providers } from 'ethers'; import { @@ -17,12 +16,13 @@ import { import { Address, WithAddress, + assert, concurrentMap, eqAddress, rootLogger, } from '@hyperlane-xyz/utils'; -import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js'; +import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainNameOrId } from '../types.js'; diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index c185d14426..27c936509c 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -65,7 +65,7 @@ export { coreFactories, } from './core/contracts.js'; export { HyperlaneLifecyleEvent } from './core/events.js'; -export { EvmCoreReader } from './core/read.js'; +export { EvmCoreReader } from './core/EvmCoreReader.js'; export { CoreConfig, CoreViolationType, @@ -124,7 +124,7 @@ export { IgpViolationType, } from './gas/types.js'; export { HyperlaneHookDeployer } from './hook/HyperlaneHookDeployer.js'; -export { EvmHookReader } from './hook/read.js'; +export { EvmHookReader } from './hook/EvmHookReader.js'; export { AggregationHookConfig, DomainRoutingHookConfig, @@ -143,7 +143,7 @@ export { buildAggregationIsmConfigs, buildMultisigIsmConfigs, } from './ism/multisig.js'; -export { EvmIsmReader } from './ism/read.js'; +export { EvmIsmReader } from './ism/EvmIsmReader.js'; export { AggregationIsmConfig, DeployedIsm, @@ -303,6 +303,17 @@ export { defaultViemProviderBuilder, protocolToDefaultProviderBuilder, } from './providers/providerBuilders.js'; +export { TxSubmitterInterface } from './providers/transactions/submitter/TxSubmitterInterface.js'; +export { TxSubmitterType } from './providers/transactions/submitter/TxSubmitterTypes.js'; +export { TxSubmitterBuilder } from './providers/transactions/submitter/builder/TxSubmitterBuilder.js'; +export { EV5GnosisSafeTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.js'; +export { EV5ImpersonatedAccountTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.js'; +export { EV5JsonRpcTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.js'; +export { EV5TxSubmitterInterface } from './providers/transactions/submitter/ethersV5/EV5TxSubmitterInterface.js'; +export { TxTransformerInterface } from './providers/transactions/transformer/TxTransformerInterface.js'; +export { TxTransformerType } from './providers/transactions/transformer/TxTransformerTypes.js'; +export { EV5InterchainAccountTxTransformer } from './providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.js'; +export { EV5TxTransformerInterface } from './providers/transactions/transformer/ethersV5/EV5TxTransformerInterface.js'; export { GasRouterDeployer } from './router/GasRouterDeployer.js'; export { HyperlaneRouterChecker } from './router/HyperlaneRouterChecker.js'; export { HyperlaneRouterDeployer } from './router/HyperlaneRouterDeployer.js'; @@ -442,6 +453,7 @@ export { setFork, stopImpersonatingAccount, } from './utils/fork.js'; + export { multisigIsmVerificationCost } from './utils/ism.js'; export { SealevelAccountDataWrapper, @@ -466,3 +478,7 @@ export { TokenRouterConfigSchema as tokenRouterConfigSchema, } from './token/schemas.js'; export { TokenRouterConfig, WarpRouteDeployConfig } from './token/types.js'; + +// prettier-ignore +// @ts-ignore +export { canProposeSafeTransactions, getSafe, getSafeDelegates, getSafeService } from './utils/gnosisSafe.js'; diff --git a/typescript/sdk/src/ism/EvmIsmCreator.ts b/typescript/sdk/src/ism/EvmIsmCreator.ts new file mode 100644 index 0000000000..2b95e3c3da --- /dev/null +++ b/typescript/sdk/src/ism/EvmIsmCreator.ts @@ -0,0 +1,575 @@ +import { ethers } from 'ethers'; +import { Logger } from 'pino'; + +import { + DefaultFallbackRoutingIsm, + DefaultFallbackRoutingIsm__factory, + DomainRoutingIsm, + DomainRoutingIsm__factory, + IAggregationIsm, + IAggregationIsm__factory, + IInterchainSecurityModule__factory, + IMultisigIsm, + IMultisigIsm__factory, + IRoutingIsm, + OPStackIsm__factory, + PausableIsm__factory, + StaticAddressSetFactory, + StaticThresholdAddressSetFactory, + TestIsm__factory, + TrustedRelayerIsm__factory, +} from '@hyperlane-xyz/core'; +import { + Address, + Domain, + assert, + eqAddress, + objFilter, + rootLogger, +} from '@hyperlane-xyz/utils'; + +import { HyperlaneContracts } from '../contracts/types.js'; +import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; +import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { resolveOrDeployAccountOwner } from '../deploy/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { ChainMap, ChainName } from '../types.js'; + +import { + AggregationIsmConfig, + DeployedIsm, + DeployedIsmType, + IsmConfig, + IsmType, + MultisigIsmConfig, + RoutingIsmConfig, + RoutingIsmDelta, +} from './types.js'; +import { routingModuleDelta } from './utils.js'; + +export class EvmIsmCreator { + protected readonly logger = rootLogger.child({ module: 'EvmIsmCreator' }); + + constructor( + protected readonly deployer: HyperlaneDeployer, + protected readonly multiProvider: MultiProvider, + protected readonly factories: HyperlaneContracts, + ) {} + + async update(params: { + destination: ChainName; + config: C; + origin?: ChainName; + mailbox?: Address; + existingIsmAddress: Address; + }): Promise { + const { destination, config, origin, mailbox, existingIsmAddress } = params; + if (typeof config === 'string') { + // @ts-ignore + return IInterchainSecurityModule__factory.connect( + config, + this.multiProvider.getSignerOrProvider(destination), + ); + } + + const ismType = config.type; + const logger = this.logger.child({ destination, ismType }); + + logger.debug( + `Updating ${ismType} on ${destination} ${ + origin ? `(for verifying ${origin})` : '' + }`, + ); + + let contract: DeployedIsmType[typeof ismType]; + switch (ismType) { + case IsmType.ROUTING: + case IsmType.FALLBACK_ROUTING: + contract = await this.updateRoutingIsm({ + destination, + config, + origin, + mailbox, + existingIsmAddress, + logger, + }); + break; + default: + return this.deploy(params); // TODO: tidy-up update in follow-up PR + } + + return contract; + } + + async deploy(params: { + destination: ChainName; + config: C; + origin?: ChainName; + mailbox?: Address; + }): Promise { + const { destination, config, origin, mailbox } = params; + if (typeof config === 'string') { + // @ts-ignore + return IInterchainSecurityModule__factory.connect( + config, + this.multiProvider.getSignerOrProvider(destination), + ); + } + + const ismType = config.type; + const logger = this.logger.child({ destination, ismType }); + + logger.debug( + `Deploying ${ismType} to ${destination} ${ + origin ? `(for verifying ${origin})` : '' + }`, + ); + + let contract: DeployedIsmType[typeof ismType]; + switch (ismType) { + case IsmType.MESSAGE_ID_MULTISIG: + case IsmType.MERKLE_ROOT_MULTISIG: + contract = await this.deployMultisigIsm(destination, config, logger); + break; + case IsmType.ROUTING: + case IsmType.FALLBACK_ROUTING: + contract = await this.deployRoutingIsm({ + destination, + config, + origin, + mailbox, + logger, + }); + break; + case IsmType.AGGREGATION: + contract = await this.deployAggregationIsm({ + destination, + config, + origin, + mailbox, + logger, + }); + break; + case IsmType.OP_STACK: + assert( + this.deployer, + `HyperlaneDeployer must be set to deploy ${ismType}`, + ); + contract = await this.deployer.deployContractFromFactory( + destination, + new OPStackIsm__factory(), + IsmType.OP_STACK, + [config.nativeBridge], + ); + break; + case IsmType.PAUSABLE: + assert( + this.deployer, + `HyperlaneDeployer must be set to deploy ${ismType}`, + ); + contract = await this.deployer.deployContractFromFactory( + destination, + new PausableIsm__factory(), + IsmType.PAUSABLE, + [ + await resolveOrDeployAccountOwner( + this.multiProvider, + destination, + config.owner, + ), + ], + ); + await this.deployer.transferOwnershipOfContracts(destination, config, { + [IsmType.PAUSABLE]: contract, + }); + break; + case IsmType.TRUSTED_RELAYER: + assert( + this.deployer, + `HyperlaneDeployer must be set to deploy ${ismType}`, + ); + assert(mailbox, `Mailbox address is required for deploying ${ismType}`); + contract = await this.deployer.deployContractFromFactory( + destination, + new TrustedRelayerIsm__factory(), + IsmType.TRUSTED_RELAYER, + [mailbox, config.relayer], + ); + break; + case IsmType.TEST_ISM: + if (!this.deployer) { + throw new Error(`HyperlaneDeployer must be set to deploy ${ismType}`); + } + contract = await this.deployer.deployContractFromFactory( + destination, + new TestIsm__factory(), + IsmType.TEST_ISM, + [], + ); + break; + default: + throw new Error(`Unsupported ISM type ${ismType}`); + } + + return contract; + } + + protected async deployMultisigIsm( + destination: ChainName, + config: MultisigIsmConfig, + logger: Logger, + ): Promise { + const signer = this.multiProvider.getSigner(destination); + const multisigIsmFactory = + config.type === IsmType.MERKLE_ROOT_MULTISIG + ? this.factories.staticMerkleRootMultisigIsmFactory + : this.factories.staticMessageIdMultisigIsmFactory; + + const address = await this.deployStaticAddressSet( + destination, + multisigIsmFactory, + config.validators, + logger, + config.threshold, + ); + + return IMultisigIsm__factory.connect(address, signer); + } + + protected async updateRoutingIsm(params: { + destination: ChainName; + config: RoutingIsmConfig; + origin?: ChainName; + mailbox?: Address; + existingIsmAddress: Address; + logger: Logger; + }): Promise { + const { destination, config, mailbox, existingIsmAddress, logger } = params; + const overrides = this.multiProvider.getTransactionOverrides(destination); + let routingIsm: DomainRoutingIsm | DefaultFallbackRoutingIsm; + + // filtering out domains which are not part of the multiprovider + config.domains = objFilter( + config.domains, + (domain, config): config is IsmConfig => { + const domainId = this.multiProvider.tryGetDomainId(domain); + if (domainId === null) { + logger.warn( + `Domain ${domain} doesn't have chain metadata provided, skipping ...`, + ); + } + return domainId !== null; + }, + ); + + const safeConfigDomains = Object.keys(config.domains).map((domain) => + this.multiProvider.getDomainId(domain), + ); + + const delta: RoutingIsmDelta = existingIsmAddress + ? await routingModuleDelta( + destination, + existingIsmAddress, + config, + this.multiProvider, + this.factories, + mailbox, + ) + : { + domainsToUnenroll: [], + domainsToEnroll: safeConfigDomains, + }; + + const signer = this.multiProvider.getSigner(destination); + const provider = this.multiProvider.getProvider(destination); + const owner = await DomainRoutingIsm__factory.connect( + existingIsmAddress, + provider, + ).owner(); + const isOwner = eqAddress(await signer.getAddress(), owner); + + // reconfiguring existing routing ISM + if (existingIsmAddress && isOwner && !delta.mailbox) { + const isms: Record = {}; + routingIsm = DomainRoutingIsm__factory.connect( + existingIsmAddress, + this.multiProvider.getSigner(destination), + ); + // deploying all the ISMs which have to be updated + for (const originDomain of delta.domainsToEnroll) { + const origin = this.multiProvider.getChainName(originDomain); // already filtered to only include domains in the multiprovider + logger.debug( + `Reconfiguring preexisting routing ISM at for origin ${origin}...`, + ); + const ism = await this.deploy({ + destination, + config: config.domains[origin], + origin, + mailbox, + }); + isms[originDomain] = ism.address; + const tx = await routingIsm.set( + originDomain, + isms[originDomain], + overrides, + ); + await this.multiProvider.handleTx(destination, tx); + } + // unenrolling domains if needed + for (const originDomain of delta.domainsToUnenroll) { + logger.debug( + `Unenrolling originDomain ${originDomain} from preexisting routing ISM at ${existingIsmAddress}...`, + ); + const tx = await routingIsm.remove(originDomain, overrides); + await this.multiProvider.handleTx(destination, tx); + } + // transfer ownership if needed + if (delta.owner) { + logger.debug(`Transferring ownership of routing ISM...`); + const tx = await routingIsm.transferOwnership(delta.owner, overrides); + await this.multiProvider.handleTx(destination, tx); + } + } else { + const isms: ChainMap
= {}; + const owner = await resolveOrDeployAccountOwner( + this.multiProvider, + destination, + config.owner, + ); + + for (const origin of Object.keys(config.domains)) { + const ism = await this.deploy({ + destination, + config: config.domains[origin], + origin, + mailbox, + }); + isms[origin] = ism.address; + } + const submoduleAddresses = Object.values(isms); + + if (config.type === IsmType.FALLBACK_ROUTING) { + // deploying new fallback routing ISM + if (!mailbox) { + throw new Error( + 'Mailbox address is required for deploying fallback routing ISM', + ); + } + + // connect to existing ISM + routingIsm = DefaultFallbackRoutingIsm__factory.connect( + existingIsmAddress, + signer, + ); + + // update ISM with config + logger.debug('Initialising fallback routing ISM ...'); + await this.multiProvider.handleTx( + destination, + routingIsm['initialize(address,uint32[],address[])']( + owner, + safeConfigDomains, + submoduleAddresses, + overrides, + ), + ); + } else { + routingIsm = await this.deployDomainRoutingIsm({ + destination, + owner, + safeConfigDomains, + submoduleAddresses, + overrides, + }); + } + } + return routingIsm; + } + + protected async deployRoutingIsm(params: { + destination: ChainName; + config: RoutingIsmConfig; + origin?: ChainName; + mailbox?: Address; + logger: Logger; + }): Promise { + const { destination, config, mailbox, logger } = params; + const overrides = this.multiProvider.getTransactionOverrides(destination); + let routingIsm: DomainRoutingIsm | DefaultFallbackRoutingIsm; + + // filtering out domains which are not part of the multiprovider + config.domains = objFilter( + config.domains, + (domain, config): config is IsmConfig => { + const domainId = this.multiProvider.tryGetDomainId(domain); + if (domainId === null) { + logger.warn( + `Domain ${domain} doesn't have chain metadata provided, skipping ...`, + ); + } + return domainId !== null; + }, + ); + + const safeConfigDomains = Object.keys(config.domains).map((domain) => + this.multiProvider.getDomainId(domain), + ); + + const isms: ChainMap
= {}; + const owner = await resolveOrDeployAccountOwner( + this.multiProvider, + destination, + config.owner, + ); + + for (const origin of Object.keys(config.domains)) { + const ism = await this.deploy({ + destination, + config: config.domains[origin], + origin, + mailbox, + }); + isms[origin] = ism.address; + } + + const submoduleAddresses = Object.values(isms); + + if (config.type === IsmType.FALLBACK_ROUTING) { + // deploying new fallback routing ISM + if (!mailbox) { + throw new Error( + 'Mailbox address is required for deploying fallback routing ISM', + ); + } + logger.debug('Deploying fallback routing ISM ...'); + routingIsm = await this.multiProvider.handleDeploy( + destination, + new DefaultFallbackRoutingIsm__factory(), + [mailbox], + ); + } else { + routingIsm = await this.deployDomainRoutingIsm({ + destination, + owner, + safeConfigDomains, + submoduleAddresses, + overrides, + }); + } + + return routingIsm; + } + + protected async deployDomainRoutingIsm(params: { + destination: ChainName; + owner: string; + safeConfigDomains: number[]; + submoduleAddresses: string[]; + overrides: ethers.Overrides; + }): Promise { + const { + destination, + owner, + safeConfigDomains, + submoduleAddresses, + overrides, + } = params; + + // deploying new domain routing ISM + const tx = await this.factories.domainRoutingIsmFactory.deploy( + owner, + safeConfigDomains, + submoduleAddresses, + overrides, + ); + + const receipt = await this.multiProvider.handleTx(destination, tx); + + // TODO: Break this out into a generalized function + const dispatchLogs = receipt.logs + .map((log) => { + try { + return this.factories.domainRoutingIsmFactory.interface.parseLog(log); + } catch (e) { + return undefined; + } + }) + .filter( + (log): log is ethers.utils.LogDescription => + !!log && log.name === 'ModuleDeployed', + ); + if (dispatchLogs.length === 0) { + throw new Error('No ModuleDeployed event found'); + } + const moduleAddress = dispatchLogs[0].args['module']; + return DomainRoutingIsm__factory.connect( + moduleAddress, + this.multiProvider.getSigner(destination), + ); + } + + protected async deployAggregationIsm(params: { + destination: ChainName; + config: AggregationIsmConfig; + origin?: ChainName; + mailbox?: Address; + logger: Logger; + }): Promise { + const { destination, config, origin, mailbox } = params; + const signer = this.multiProvider.getSigner(destination); + const staticAggregationIsmFactory = + this.factories.staticAggregationIsmFactory; + const addresses: Address[] = []; + for (const module of config.modules) { + const submodule = await this.deploy({ + destination, + config: module, + origin, + mailbox, + }); + addresses.push(submodule.address); + } + const address = await this.deployStaticAddressSet( + destination, + staticAggregationIsmFactory, + addresses, + params.logger, + config.threshold, + ); + return IAggregationIsm__factory.connect(address, signer); + } + + async deployStaticAddressSet( + chain: ChainName, + factory: StaticThresholdAddressSetFactory | StaticAddressSetFactory, + values: Address[], + logger: Logger, + threshold = values.length, + ): Promise
{ + const sorted = [...values].sort(); + + const address = await factory['getAddress(address[],uint8)']( + sorted, + threshold, + ); + const code = await this.multiProvider.getProvider(chain).getCode(address); + if (code === '0x') { + logger.debug( + `Deploying new ${threshold} of ${values.length} address set to ${chain}`, + ); + const overrides = this.multiProvider.getTransactionOverrides(chain); + const hash = await factory['deploy(address[],uint8)']( + sorted, + threshold, + overrides, + ); + await this.multiProvider.handleTx(chain, hash); + // TODO: add proxy verification artifact? + } else { + logger.debug( + `Recovered ${threshold} of ${values.length} address set on ${chain}: ${address}`, + ); + } + return address; + } +} diff --git a/typescript/sdk/src/ism/EvmIsmModule.ts b/typescript/sdk/src/ism/EvmIsmModule.ts new file mode 100644 index 0000000000..7d9921832b --- /dev/null +++ b/typescript/sdk/src/ism/EvmIsmModule.ts @@ -0,0 +1,89 @@ +import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { HyperlaneContracts } from '../contracts/types.js'; +import { + HyperlaneModule, + HyperlaneModuleArgs, +} from '../core/AbstractHyperlaneModule.js'; +import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; +import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { EthersV5Transaction } from '../providers/ProviderType.js'; +import { ChainNameOrId } from '../types.js'; + +import { EvmIsmCreator } from './EvmIsmCreator.js'; +import { EvmIsmReader } from './EvmIsmReader.js'; +import { IsmConfig } from './types.js'; + +export class EvmIsmModule extends HyperlaneModule< + ProtocolType.Ethereum, + IsmConfig, + HyperlaneContracts & { + deployedIsm: Address; + } +> { + protected logger = rootLogger.child({ module: 'EvmIsmModule' }); + protected reader: EvmIsmReader; + protected creator: EvmIsmCreator; + + protected constructor( + protected readonly multiProvider: MultiProvider, + protected readonly deployer: HyperlaneDeployer, + args: HyperlaneModuleArgs< + IsmConfig, + HyperlaneContracts & { + deployedIsm: Address; + } + >, + ) { + super(args); + this.reader = new EvmIsmReader(multiProvider, args.chain); + this.creator = new EvmIsmCreator(deployer, multiProvider, args.addresses); + } + + public async read(): Promise { + return await this.reader.deriveIsmConfig(this.args.addresses.deployedIsm); + } + + public async update(config: IsmConfig): Promise { + throw new Error('Method not implemented.'); + + const destination = this.multiProvider.getChainName(this.args.chain); + await this.creator.update({ + destination, + config, + existingIsmAddress: this.args.addresses.deployedIsm, + }); + return []; + } + + // manually write static create function + public static async create({ + chain, + config, + deployer, + factories, + multiProvider, + }: { + chain: ChainNameOrId; + config: IsmConfig; + deployer: HyperlaneDeployer; + factories: HyperlaneContracts; + multiProvider: MultiProvider; + }): Promise { + const destination = multiProvider.getChainName(chain); + const ismCreator = new EvmIsmCreator(deployer, multiProvider, factories); + const deployedIsm = await ismCreator.deploy({ + config, + destination, + }); + return new EvmIsmModule(multiProvider, deployer, { + addresses: { + ...factories, + deployedIsm: deployedIsm.address, + }, + chain, + config, + }); + } +} diff --git a/typescript/sdk/src/ism/read.test.ts b/typescript/sdk/src/ism/EvmIsmReader.test.ts similarity index 99% rename from typescript/sdk/src/ism/read.test.ts rename to typescript/sdk/src/ism/EvmIsmReader.test.ts index a50287de63..95368949b3 100644 --- a/typescript/sdk/src/ism/read.test.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.test.ts @@ -21,7 +21,7 @@ import { WithAddress } from '@hyperlane-xyz/utils'; import { TestChainName } from '../consts/testChains.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { EvmIsmReader } from './read.js'; +import { EvmIsmReader } from './EvmIsmReader.js'; import { IsmType, ModuleType, diff --git a/typescript/sdk/src/ism/read.ts b/typescript/sdk/src/ism/EvmIsmReader.ts similarity index 97% rename from typescript/sdk/src/ism/read.ts rename to typescript/sdk/src/ism/EvmIsmReader.ts index 979550bcc4..0992ad2f07 100644 --- a/typescript/sdk/src/ism/read.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.ts @@ -18,7 +18,7 @@ import { rootLogger, } from '@hyperlane-xyz/utils'; -import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js'; +import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainNameOrId } from '../types.js'; @@ -95,11 +95,7 @@ export class EvmIsmReader implements IsmReader { case ModuleType.MESSAGE_ID_MULTISIG: return this.deriveMultisigConfig(address); case ModuleType.NULL: - return { - type: IsmType.TEST_ISM, - address, - }; - // return this.deriveNullConfig(address); + return this.deriveNullConfig(address); case ModuleType.CCIP_READ: throw new Error('CCIP_READ does not have a corresponding IsmType'); default: diff --git a/typescript/sdk/src/ism/metadata/aggregation.test.ts b/typescript/sdk/src/ism/metadata/aggregation.test.ts index b7646dccc0..cb6c3818ca 100644 --- a/typescript/sdk/src/ism/metadata/aggregation.test.ts +++ b/typescript/sdk/src/ism/metadata/aggregation.test.ts @@ -5,15 +5,11 @@ import { AggregationIsmMetadata, AggregationIsmMetadataBuilder, } from './aggregation.js'; - -type Fixture = { - decoded: AggregationIsmMetadata; - encoded: string; -}; +import { Fixture } from './types.test.js'; const path = '../../solidity/fixtures/aggregation'; const files = readdirSync(path); -const fixtures: Fixture[] = files +const fixtures: Fixture[] = files .map((f) => JSON.parse(readFileSync(`${path}/${f}`, 'utf8'))) .map((contents) => { const { encoded, ...values } = contents; diff --git a/typescript/sdk/src/ism/metadata/aggregation.ts b/typescript/sdk/src/ism/metadata/aggregation.ts index 53a9396e96..8980c9a6e7 100644 --- a/typescript/sdk/src/ism/metadata/aggregation.ts +++ b/typescript/sdk/src/ism/metadata/aggregation.ts @@ -9,8 +9,8 @@ import { } from '@hyperlane-xyz/utils'; import { DispatchedMessage } from '../../core/types.js'; -import { DerivedHookConfigWithAddress } from '../../hook/read.js'; -import { DerivedIsmConfigWithAddress } from '../read.js'; +import { DerivedHookConfigWithAddress } from '../../hook/EvmHookReader.js'; +import { DerivedIsmConfigWithAddress } from '../EvmIsmReader.js'; import { AggregationIsmConfig } from '../types.js'; import { BaseMetadataBuilder, MetadataBuilder } from './builder.js'; @@ -81,9 +81,7 @@ export class AggregationIsmMetadataBuilder let encoded = Buffer.alloc(rangeSize, 0); metadata.submoduleMetadata.forEach((meta, index) => { - if (meta === null) { - return; - } + if (!meta) return; const start = encoded.length; encoded = Buffer.concat([encoded, fromHexString(meta)]); diff --git a/typescript/sdk/src/ism/metadata/builder.ts b/typescript/sdk/src/ism/metadata/builder.ts index 97bdf9be20..dca5219b70 100644 --- a/typescript/sdk/src/ism/metadata/builder.ts +++ b/typescript/sdk/src/ism/metadata/builder.ts @@ -10,9 +10,9 @@ import { import { deepFind } from '../../../../utils/dist/objects.js'; import { HyperlaneCore } from '../../core/HyperlaneCore.js'; import { DispatchedMessage } from '../../core/types.js'; -import { DerivedHookConfigWithAddress } from '../../hook/read.js'; +import { DerivedHookConfigWithAddress } from '../../hook/EvmHookReader.js'; import { HookType, MerkleTreeHookConfig } from '../../hook/types.js'; -import { DerivedIsmConfigWithAddress } from '../read.js'; +import { DerivedIsmConfigWithAddress } from '../EvmIsmReader.js'; import { IsmType } from '../types.js'; import { diff --git a/typescript/sdk/src/ism/metadata/multisig.test.ts b/typescript/sdk/src/ism/metadata/multisig.test.ts index 384dc457ce..1afca2bdb1 100644 --- a/typescript/sdk/src/ism/metadata/multisig.test.ts +++ b/typescript/sdk/src/ism/metadata/multisig.test.ts @@ -6,15 +6,11 @@ import { SignatureLike } from '@hyperlane-xyz/utils'; import { ModuleType } from '../types.js'; import { MultisigMetadata, MultisigMetadataBuilder } from './multisig.js'; - -type Fixture = { - decoded: MultisigMetadata; - encoded: string; -}; +import { Fixture } from './types.test.js'; const path = '../../solidity/fixtures/multisig'; const files = readdirSync(path); -const fixtures: Fixture[] = files +const fixtures: Fixture[] = files .map((f) => JSON.parse(readFileSync(`${path}/${f}`, 'utf8'))) .map((contents) => { const type = contents.type as MultisigMetadata['type']; diff --git a/typescript/sdk/src/ism/metadata/multisig.ts b/typescript/sdk/src/ism/metadata/multisig.ts index b6bc6e7443..b91cd89795 100644 --- a/typescript/sdk/src/ism/metadata/multisig.ts +++ b/typescript/sdk/src/ism/metadata/multisig.ts @@ -21,7 +21,6 @@ import { toHexString, } from '@hyperlane-xyz/utils'; -import '../../../../utils/dist/types.js'; import { S3Validator } from '../../aws/validator.js'; import { HyperlaneCore } from '../../core/HyperlaneCore.js'; import { DispatchedMessage } from '../../core/types.js'; @@ -225,7 +224,7 @@ export class MultisigMetadataBuilder return toHexString(buf); } - private static decodeSimplePrefix(metadata: string) { + static decodeSimplePrefix(metadata: string) { const buf = fromHexString(metadata); const merkleTree = toHexString(buf.subarray(0, 32)); const root = toHexString(buf.subarray(32, 64)); @@ -242,9 +241,7 @@ export class MultisigMetadataBuilder }; } - private static encodeProofPrefix( - metadata: MerkleRootMultisigMetadata, - ): string { + static encodeProofPrefix(metadata: MerkleRootMultisigMetadata): string { const checkpoint = metadata.checkpoint; const buf = Buffer.alloc(1096); buf.write(strip0x(checkpoint.merkle_tree_hook_address), 0, 32, 'hex'); @@ -258,7 +255,7 @@ export class MultisigMetadataBuilder return toHexString(buf); } - private static decodeProofPrefix(metadata: string) { + static decodeProofPrefix(metadata: string) { const buf = fromHexString(metadata); const merkleTree = toHexString(buf.subarray(0, 32)); const messageIndex = buf.readUint32BE(32); @@ -299,7 +296,7 @@ export class MultisigMetadataBuilder return encoded; } - private static signatureAt( + static signatureAt( metadata: string, offset: number, index: number, diff --git a/typescript/sdk/src/ism/metadata/types.test.ts b/typescript/sdk/src/ism/metadata/types.test.ts new file mode 100644 index 0000000000..b173f3ca35 --- /dev/null +++ b/typescript/sdk/src/ism/metadata/types.test.ts @@ -0,0 +1,4 @@ +export type Fixture = { + decoded: T; + encoded: string; +}; diff --git a/typescript/sdk/src/providers/MultiProvider.ts b/typescript/sdk/src/providers/MultiProvider.ts index ecaec67f8b..0df4567dca 100644 --- a/typescript/sdk/src/providers/MultiProvider.ts +++ b/typescript/sdk/src/providers/MultiProvider.ts @@ -348,7 +348,7 @@ export class MultiProvider extends ChainMetadataManager { tx: PopulatedTransaction, from?: string, ): Promise { - const txFrom = from ? from : await this.getSignerAddress(chainNameOrId); + const txFrom = from ?? (await this.getSignerAddress(chainNameOrId)); const overrides = this.getTransactionOverrides(chainNameOrId); return { ...tx, diff --git a/typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts b/typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts new file mode 100644 index 0000000000..b857bd990d --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts @@ -0,0 +1,32 @@ +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { ChainName } from '../../../types.js'; +import { + ProtocolTypedProvider, + ProtocolTypedReceipt, + ProtocolTypedTransaction, +} from '../../ProviderType.js'; + +import { TxSubmitterType } from './TxSubmitterTypes.js'; + +export interface TxSubmitterInterface { + /** + * Defines the type of tx submitter. + */ + txSubmitterType: TxSubmitterType; + /** + * The chain to submit transactions on. + */ + chain: ChainName; + /** + * The provider to use for transaction submission. + */ + provider?: ProtocolTypedProvider['provider']; + /** + * Should execute all transactions and return their receipts. + * @param txs The array of transactions to execute + */ + submit( + ...txs: ProtocolTypedTransaction['transaction'][] + ): Promise['receipt'][] | void>; +} diff --git a/typescript/sdk/src/providers/transactions/submitter/TxSubmitterTypes.ts b/typescript/sdk/src/providers/transactions/submitter/TxSubmitterTypes.ts new file mode 100644 index 0000000000..4e38f9c25a --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/TxSubmitterTypes.ts @@ -0,0 +1,5 @@ +export enum TxSubmitterType { + JSON_RPC = 'JSON RPC', + IMPERSONATED_ACCOUNT = 'Impersonated Account', + GNOSIS_SAFE = 'Gnosis Safe', +} diff --git a/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts b/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts new file mode 100644 index 0000000000..628bda0352 --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts @@ -0,0 +1,100 @@ +import { Logger } from 'pino'; + +import { rootLogger } from '@hyperlane-xyz/utils'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { ChainName } from '../../../../types.js'; +import { + ProtocolTypedReceipt, + ProtocolTypedTransaction, +} from '../../../ProviderType.js'; +import { TxTransformerInterface } from '../../transformer/TxTransformerInterface.js'; +import { TxSubmitterInterface } from '../TxSubmitterInterface.js'; +import { TxSubmitterType } from '../TxSubmitterTypes.js'; + +/** + * Builds a TxSubmitterBuilder for batch transaction submission. + * + * Example use-cases: + * const eV5builder = new TxSubmitterBuilder(); + * let txReceipts = eV5builder.for( + * new EV5GnosisSafeTxSubmitter(chainA) + * ).transform( + * EV5InterchainAccountTxTransformer(chainB) + * ).submit( + * txs + * ); + * txReceipts = eV5builder.for( + * new EV5ImpersonatedAccountTxSubmitter(chainA) + * ).submit(txs); + * txReceipts = eV5builder.for( + * new EV5JsonRpcTxSubmitter(chainC) + * ).submit(txs); + */ +export class TxSubmitterBuilder + implements TxSubmitterInterface +{ + public readonly txSubmitterType: TxSubmitterType; + public readonly chain: ChainName; + + protected readonly logger: Logger = rootLogger.child({ + module: 'submitter-builder', + }); + + constructor( + private currentSubmitter: TxSubmitterInterface, + private currentTransformers: TxTransformerInterface[] = [], + ) { + this.txSubmitterType = this.currentSubmitter.txSubmitterType; + this.chain = this.currentSubmitter.chain; + } + + /** + * Sets the current submitter for the builder. + * @param txSubmitterOrType The submitter to add to the builder + */ + public for( + txSubmitter: TxSubmitterInterface, + ): TxSubmitterBuilder { + this.currentSubmitter = txSubmitter; + return this; + } + + /** + * Adds a transformer for the builder. + * @param txTransformerOrType The transformer to add to the builder + */ + public transform( + ...txTransformers: TxTransformerInterface[] + ): TxSubmitterBuilder { + this.currentTransformers = txTransformers; + return this; + } + + /** + * Submits a set of transactions to the builder. + * @param txs The transactions to submit + */ + public async submit( + ...txs: ProtocolTypedTransaction['transaction'][] + ): Promise['receipt'][] | void> { + this.logger.info( + `Submitting ${txs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter...`, + ); + + let transformedTxs = txs; + for (const currentTransformer of this.currentTransformers) { + transformedTxs = await currentTransformer.transform(...transformedTxs); + this.logger.info( + `🔄 Transformed ${transformedTxs.length} transactions with the ${currentTransformer.txTransformerType} transformer...`, + ); + } + + const txReceipts = await this.currentSubmitter.submit(...transformedTxs); + this.logger.info( + `✅ Successfully submitted ${transformedTxs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter.`, + ); + + return txReceipts; + } +} diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts new file mode 100644 index 0000000000..9975f560d3 --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts @@ -0,0 +1,75 @@ +import { PopulatedTransaction } from 'ethers'; +import { Logger } from 'pino'; + +import { Address, assert, rootLogger } from '@hyperlane-xyz/utils'; + +import { ChainName } from '../../../../types.js'; +// @ts-ignore +import { getSafe, getSafeService } from '../../../../utils/gnosisSafe.js'; +import { MultiProvider } from '../../../MultiProvider.js'; +import { TxSubmitterType } from '../TxSubmitterTypes.js'; + +import { EV5TxSubmitterInterface } from './EV5TxSubmitterInterface.js'; + +interface EV5GnosisSafeTxSubmitterProps { + safeAddress: Address; +} + +export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface { + public readonly txSubmitterType: TxSubmitterType = + TxSubmitterType.GNOSIS_SAFE; + + protected readonly logger: Logger = rootLogger.child({ + module: 'gnosis-safe-submitter', + }); + + constructor( + public readonly multiProvider: MultiProvider, + public readonly chain: ChainName, + public readonly props: EV5GnosisSafeTxSubmitterProps, + ) {} + + public async submit(...txs: PopulatedTransaction[]): Promise { + const safe = await getSafe( + this.chain, + this.multiProvider, + this.props.safeAddress, + ); + const safeService = await getSafeService(this.chain, this.multiProvider); + const nextNonce: number = await safeService.getNextNonce( + this.props.safeAddress, + ); + const safeTransactionBatch: any[] = txs.map( + ({ to, data, value }: PopulatedTransaction) => { + assert( + to && data, + 'Invalid PopulatedTransaction: Missing required field to or data.', + ); + return { to, data, value: value?.toString() ?? '0' }; + }, + ); + const safeTransaction = await safe.createTransaction({ + safeTransactionData: safeTransactionBatch, + options: { nonce: nextNonce }, + }); + const safeTransactionData: any = safeTransaction.data; + const safeTxHash: string = await safe.getTransactionHash(safeTransaction); + const senderAddress: Address = await this.multiProvider.getSignerAddress( + this.chain, + ); + const safeSignature: any = await safe.signTransactionHash(safeTxHash); + const senderSignature: string = safeSignature.data; + + this.logger.debug( + `Submitting transaction proposal to ${this.props.safeAddress} on ${this.chain}: ${safeTxHash}`, + ); + + return safeService.proposeTransaction({ + safeAddress: this.props.safeAddress, + safeTransactionData, + safeTxHash, + senderAddress, + senderSignature, + }); + } +} diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts new file mode 100644 index 0000000000..3511d00b16 --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts @@ -0,0 +1,43 @@ +import { TransactionReceipt } from '@ethersproject/providers'; +import { PopulatedTransaction } from 'ethers'; +import { Logger } from 'pino'; + +import { rootLogger } from '@hyperlane-xyz/utils'; +import { Address } from '@hyperlane-xyz/utils'; + +import { ChainName } from '../../../../types.js'; +import { impersonateAccount } from '../../../../utils/fork.js'; +import { MultiProvider } from '../../../MultiProvider.js'; +import { TxSubmitterType } from '../TxSubmitterTypes.js'; + +import { EV5JsonRpcTxSubmitter } from './EV5JsonRpcTxSubmitter.js'; + +interface EV5ImpersonatedAccountTxSubmitterProps { + address: Address; +} + +export class EV5ImpersonatedAccountTxSubmitter extends EV5JsonRpcTxSubmitter { + public readonly txSubmitterType: TxSubmitterType = + TxSubmitterType.IMPERSONATED_ACCOUNT; + + protected readonly logger: Logger = rootLogger.child({ + module: 'impersonated-account-submitter', + }); + + constructor( + public readonly multiProvider: MultiProvider, + public readonly chain: ChainName, + public readonly props: EV5ImpersonatedAccountTxSubmitterProps, + ) { + super(multiProvider, chain); + } + + public async submit( + ...txs: PopulatedTransaction[] + ): Promise { + const impersonatedAccount = await impersonateAccount(this.props.address); + this.multiProvider.setSigner(this.chain, impersonatedAccount); + super.multiProvider.setSigner(this.chain, impersonatedAccount); + return await super.submit(...txs); + } +} diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts new file mode 100644 index 0000000000..e1077d7c5c --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts @@ -0,0 +1,41 @@ +import { TransactionReceipt } from '@ethersproject/providers'; +import { ContractReceipt, PopulatedTransaction } from 'ethers'; +import { Logger } from 'pino'; + +import { rootLogger } from '@hyperlane-xyz/utils'; + +import { ChainName } from '../../../../types.js'; +import { MultiProvider } from '../../../MultiProvider.js'; +import { TxSubmitterType } from '../TxSubmitterTypes.js'; + +import { EV5TxSubmitterInterface } from './EV5TxSubmitterInterface.js'; + +export class EV5JsonRpcTxSubmitter implements EV5TxSubmitterInterface { + public readonly txSubmitterType: TxSubmitterType = TxSubmitterType.JSON_RPC; + + protected readonly logger: Logger = rootLogger.child({ + module: 'json-rpc-submitter', + }); + + constructor( + public readonly multiProvider: MultiProvider, + public readonly chain: ChainName, + ) {} + + public async submit( + ...txs: PopulatedTransaction[] + ): Promise { + const receipts: TransactionReceipt[] = []; + for (const tx of txs) { + const receipt: ContractReceipt = await this.multiProvider.sendTransaction( + this.chain, + tx, + ); + this.logger.debug( + `Submitted PopulatedTransaction on ${this.chain}: ${receipt.transactionHash}`, + ); + receipts.push(receipt); + } + return receipts; + } +} diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5TxSubmitterInterface.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5TxSubmitterInterface.ts new file mode 100644 index 0000000000..d1c452f7d1 --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5TxSubmitterInterface.ts @@ -0,0 +1,12 @@ +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { MultiProvider } from '../../../MultiProvider.js'; +import { TxSubmitterInterface } from '../TxSubmitterInterface.js'; + +export interface EV5TxSubmitterInterface + extends TxSubmitterInterface { + /** + * The EV5 multi-provider to use for transaction submission. + */ + multiProvider: MultiProvider; +} diff --git a/typescript/sdk/src/providers/transactions/transformer/TxTransformerInterface.ts b/typescript/sdk/src/providers/transactions/transformer/TxTransformerInterface.ts new file mode 100644 index 0000000000..5f2476d9fa --- /dev/null +++ b/typescript/sdk/src/providers/transactions/transformer/TxTransformerInterface.ts @@ -0,0 +1,19 @@ +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { ProtocolTypedTransaction } from '../../ProviderType.js'; + +import { TxTransformerType } from './TxTransformerTypes.js'; + +export interface TxTransformerInterface { + /** + * Defines the type of tx transformer. + */ + txTransformerType: TxTransformerType; + /** + * Should transform all transactions of type TX into transactions of type TX. + * @param txs The array of transactions to transform + */ + transform( + ...txs: ProtocolTypedTransaction['transaction'][] + ): Promise['transaction'][]>; +} diff --git a/typescript/sdk/src/providers/transactions/transformer/TxTransformerTypes.ts b/typescript/sdk/src/providers/transactions/transformer/TxTransformerTypes.ts new file mode 100644 index 0000000000..b8e029b2c1 --- /dev/null +++ b/typescript/sdk/src/providers/transactions/transformer/TxTransformerTypes.ts @@ -0,0 +1,3 @@ +export enum TxTransformerType { + ICA = 'Interchain Account', +} diff --git a/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts new file mode 100644 index 0000000000..2e2ddfb520 --- /dev/null +++ b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts @@ -0,0 +1,61 @@ +import { PopulatedTransaction } from 'ethers'; +import { Logger } from 'pino'; + +import { CallData, assert, rootLogger } from '@hyperlane-xyz/utils'; + +import { InterchainAccount } from '../../../../middleware/account/InterchainAccount.js'; +import { AccountConfig } from '../../../../middleware/account/types.js'; +import { ChainName } from '../../../../types.js'; +import { MultiProvider } from '../../../MultiProvider.js'; +import { TxTransformerType } from '../TxTransformerTypes.js'; + +import { EV5TxTransformerInterface } from './EV5TxTransformerInterface.js'; + +interface EV5InterchainAccountTxTransformerProps { + interchainAccount: InterchainAccount; + accountConfig: AccountConfig; + hookMetadata?: string; +} + +export class EV5InterchainAccountTxTransformer + implements EV5TxTransformerInterface +{ + public readonly txTransformerType: TxTransformerType = TxTransformerType.ICA; + protected readonly logger: Logger = rootLogger.child({ + module: 'ica-transformer', + }); + + constructor( + public readonly multiProvider: MultiProvider, + public readonly chain: ChainName, + public readonly props: EV5InterchainAccountTxTransformerProps, + ) {} + + public async transform( + ...txs: PopulatedTransaction[] + ): Promise { + const destinationChainId = txs[0].chainId; + assert( + destinationChainId, + 'Missing destination chainId in PopulatedTransaction.', + ); + + const innerCalls: CallData[] = txs.map( + ({ to, data, value }: PopulatedTransaction) => { + assert(to, 'Invalid PopulatedTransaction: Missing to field'); + assert(data, 'Invalid PopulatedTransaction: Missing data field'); + return { to, data, value }; + }, + ); + + return [ + await this.props.interchainAccount.getCallRemote( + this.chain, + this.multiProvider.getChainName(this.chain), //chainIdToMetadata[destinationChainId].name, + innerCalls, + this.props.accountConfig, + this.props.hookMetadata, + ), + ]; + } +} diff --git a/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5TxTransformerInterface.ts b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5TxTransformerInterface.ts new file mode 100644 index 0000000000..32e3c23f2b --- /dev/null +++ b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5TxTransformerInterface.ts @@ -0,0 +1,6 @@ +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { TxTransformerInterface } from '../TxTransformerInterface.js'; + +export interface EV5TxTransformerInterface + extends TxTransformerInterface {} diff --git a/typescript/sdk/src/token/read.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts similarity index 95% rename from typescript/sdk/src/token/read.ts rename to typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 38c093b321..0699fe5c45 100644 --- a/typescript/sdk/src/token/read.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -7,9 +7,9 @@ import { import { ERC20Metadata, ERC20RouterConfig } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; -import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js'; -import { EvmHookReader } from '../hook/read.js'; -import { EvmIsmReader } from '../ism/read.js'; +import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; +import { EvmHookReader } from '../hook/EvmHookReader.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index 53817e3628..3a1e6414d1 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -47,6 +47,24 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'USDC', name: 'USDC', }, + [TokenStandard.EvmHypXERC20Collateral]: { + chainName: TestChainName.test3, + standard: TokenStandard.EvmHypXERC20Collateral, + addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131', + collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930', + decimals: 18, + symbol: 'USDC', + name: 'USDC', + }, + [TokenStandard.EvmHypFiatCollateral]: { + chainName: TestChainName.test3, + standard: TokenStandard.EvmHypXERC20Collateral, + addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131', + collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930', + decimals: 18, + symbol: 'USDC', + name: 'USDC', + }, [TokenStandard.EvmHypCollateralVault]: { chainName: TestChainName.test3, standard: TokenStandard.EvmHypCollateral, diff --git a/typescript/sdk/src/token/Token.ts b/typescript/sdk/src/token/Token.ts index 7ddc870e57..5c0e0af649 100644 --- a/typescript/sdk/src/token/Token.ts +++ b/typescript/sdk/src/token/Token.ts @@ -205,7 +205,12 @@ export class Token implements IToken { return new EvmHypNativeAdapter(chainName, multiProvider, { token: addressOrDenom, }); - } else if (standard === TokenStandard.EvmHypCollateral) { + } else if ( + standard === TokenStandard.EvmHypCollateral || + standard === TokenStandard.EvmHypCollateralVault || + standard === TokenStandard.EvmHypXERC20Collateral || + standard === TokenStandard.EvmHypFiatCollateral + ) { return new EvmHypCollateralAdapter(chainName, multiProvider, { token: addressOrDenom, }); diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index f2958304e7..d1394d49c9 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -14,6 +14,8 @@ export enum TokenStandard { EvmNative = 'EvmNative', EvmHypNative = 'EvmHypNative', EvmHypCollateral = 'EvmHypCollateral', + EvmHypXERC20Collateral = 'EvmHypXERC20Collateral', + EvmHypFiatCollateral = 'EvmHypFiatCollateral', EvmHypCollateralVault = 'EvmHypCollateralVault', EvmHypSynthetic = 'EvmHypSynthetic', @@ -50,6 +52,8 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record = { EvmHypCollateral: ProtocolType.Ethereum, EvmHypCollateralVault: ProtocolType.Ethereum, EvmHypSynthetic: ProtocolType.Ethereum, + EvmHypXERC20Collateral: ProtocolType.Ethereum, + EvmHypFiatCollateral: ProtocolType.Ethereum, // Sealevel (Solana) SealevelSpl: ProtocolType.Sealevel, @@ -92,6 +96,8 @@ export const TOKEN_NFT_STANDARDS = [ export const TOKEN_COLLATERALIZED_STANDARDS = [ TokenStandard.EvmHypCollateral, TokenStandard.EvmHypNative, + TokenStandard.EvmHypXERC20Collateral, + TokenStandard.EvmHypFiatCollateral, TokenStandard.SealevelHypCollateral, TokenStandard.SealevelHypNative, TokenStandard.CwHypCollateral, @@ -101,6 +107,8 @@ export const TOKEN_COLLATERALIZED_STANDARDS = [ export const TOKEN_HYP_STANDARDS = [ TokenStandard.EvmHypNative, TokenStandard.EvmHypCollateral, + TokenStandard.EvmHypXERC20Collateral, + TokenStandard.EvmHypFiatCollateral, TokenStandard.EvmHypSynthetic, TokenStandard.SealevelHypNative, TokenStandard.SealevelHypCollateral, @@ -129,6 +137,8 @@ export const TOKEN_COSMWASM_STANDARDS = [ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.native]: TokenStandard.EvmHypNative, [TokenType.collateral]: TokenStandard.EvmHypCollateral, + [TokenType.collateralFiat]: TokenStandard.EvmHypFiatCollateral, + [TokenType.collateralXERC20]: TokenStandard.EvmHypXERC20Collateral, [TokenType.collateralVault]: TokenStandard.EvmHypCollateralVault, [TokenType.collateralUri]: TokenStandard.EvmHypCollateral, [TokenType.fastCollateral]: TokenStandard.EvmHypCollateral, diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index ca1436878f..e8d57a4a7d 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -11,6 +11,8 @@ export enum TokenType { syntheticUri = 'syntheticUri', collateral = 'collateral', collateralVault = 'collateralVault', + collateralXERC20 = 'collateralXERC20', + collateralFiat = 'collateralFiat', fastCollateral = 'fastCollateral', collateralUri = 'collateralUri', native = 'native', @@ -41,6 +43,8 @@ export type SyntheticConfig = z.infer; export type CollateralConfig = { type: | TokenType.collateral + | TokenType.collateralXERC20 + | TokenType.collateralFiat | TokenType.collateralUri | TokenType.fastCollateral | TokenType.fastSynthetic @@ -57,6 +61,8 @@ export const isCollateralConfig = ( config: TokenConfig, ): config is CollateralConfig => config.type === TokenType.collateral || + config.type === TokenType.collateralXERC20 || + config.type === TokenType.collateralFiat || config.type === TokenType.collateralUri || config.type === TokenType.fastCollateral || config.type == TokenType.collateralVault; diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index 766e4ca759..50ef36dbb0 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -8,8 +8,10 @@ import { HypERC721URICollateral__factory, HypERC721URIStorage__factory, HypERC721__factory, + HypFiatTokenCollateral__factory, HypNativeScaled__factory, HypNative__factory, + HypXERC20Collateral__factory, } from '@hyperlane-xyz/core'; import { proxiedFactories } from '../router/types.js'; @@ -21,6 +23,8 @@ export const hypERC20contracts = { [TokenType.fastSynthetic]: 'FastHypERC20', [TokenType.synthetic]: 'HypERC20', [TokenType.collateral]: 'HypERC20Collateral', + [TokenType.collateralFiat]: 'HypFiatTokenCollateral', + [TokenType.collateralXERC20]: 'HypXERC20Collateral', [TokenType.collateralVault]: 'HypERC20CollateralVaultDeposit', [TokenType.native]: 'HypNative', [TokenType.nativeScaled]: 'HypNativeScaled', @@ -33,6 +37,8 @@ export const hypERC20Tokenfactories = { [TokenType.synthetic]: new HypERC20__factory(), [TokenType.collateral]: new HypERC20Collateral__factory(), [TokenType.collateralVault]: new HypERC20CollateralVaultDeposit__factory(), + [TokenType.collateralFiat]: new HypFiatTokenCollateral__factory(), + [TokenType.collateralXERC20]: new HypXERC20Collateral__factory(), [TokenType.native]: new HypNative__factory(), [TokenType.nativeScaled]: new HypNativeScaled__factory(), }; diff --git a/typescript/sdk/src/token/deploy.hardhat-test.ts b/typescript/sdk/src/token/deploy.hardhat-test.ts index 72a3028481..25ed9e50bf 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -17,6 +17,7 @@ import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap } from '../types.js'; +import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; import { HypERC20CollateralConfig, HypERC20Config, @@ -24,7 +25,6 @@ import { TokenType, } from './config.js'; import { HypERC20Deployer } from './deploy.js'; -import { EvmERC20WarpRouteReader } from './read.js'; import { WarpRouteDeployConfig } from './types.js'; describe('TokenDeployer', async () => { diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index d23038cecf..f4f4e93b99 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -28,9 +28,7 @@ import { TokenMetadata, TokenType, isCollateralConfig, - isCollateralVaultConfig, isErc20Metadata, - isFastConfig, isNativeConfig, isSyntheticConfig, isTokenMetadata, @@ -38,6 +36,7 @@ import { } from './config.js'; import { HypERC20Factories, + HypERC20contracts, HypERC721Factories, HypERC721contracts, hypERC20contracts, @@ -67,23 +66,7 @@ export class HypERC20Deployer extends GasRouterDeployer< } routerContractKey(config: ERC20RouterConfig) { - if (isCollateralConfig(config)) { - if (isFastConfig(config)) { - return TokenType.fastCollateral; - } else if (isCollateralVaultConfig(config)) { - return TokenType.collateralVault; - } else { - return TokenType.collateral; - } - } else if (isNativeConfig(config)) { - return config.scale ? TokenType.nativeScaled : TokenType.native; - } else if (isSyntheticConfig(config)) { - return isFastConfig(config) - ? TokenType.fastSynthetic - : TokenType.synthetic; - } else { - throw new Error('Unknown collateral type when constructing router name'); - } + return config.type as keyof HypERC20contracts; } async constructorArgs( diff --git a/typescript/sdk/src/utils/fork.ts b/typescript/sdk/src/utils/fork.ts index 38660a6c50..51ed576e0c 100644 --- a/typescript/sdk/src/utils/fork.ts +++ b/typescript/sdk/src/utils/fork.ts @@ -5,8 +5,6 @@ import { Address, isValidAddressEvm, rootLogger } from '@hyperlane-xyz/utils'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; -const logger = rootLogger.child({ module: 'fork-utils' }); - const ENDPOINT_PREFIX = 'http'; const DEFAULT_ANVIL_ENDPOINT = 'http://127.0.0.1:8545'; @@ -21,7 +19,7 @@ export enum ANVIL_RPC_METHODS { * Resets the local node to it's original state (anvil [31337] at block zero). */ export const resetFork = async (anvilIPAddr?: string, anvilPort?: number) => { - logger.info(`Resetting forked network...`); + rootLogger.info(`Resetting forked network...`); const provider = getLocalProvider(anvilIPAddr, anvilPort); await provider.send(ANVIL_RPC_METHODS.RESET, [ @@ -32,7 +30,7 @@ export const resetFork = async (anvilIPAddr?: string, anvilPort?: number) => { }, ]); - logger.info(`✅ Successfully reset forked network`); + rootLogger.info(`✅ Successfully reset forked network`); }; /** @@ -46,7 +44,7 @@ export const setFork = async ( anvilIPAddr?: string, anvilPort?: number, ) => { - logger.info(`Forking ${chain} for dry-run...`); + rootLogger.info(`Forking ${chain} for dry-run...`); const provider = getLocalProvider(anvilIPAddr, anvilPort); const currentChainMetadata = multiProvider.metadata[chain]; @@ -61,7 +59,7 @@ export const setFork = async ( multiProvider.setProvider(chain, provider); - logger.info(`✅ Successfully forked ${chain} for dry-run`); + rootLogger.info(`✅ Successfully forked ${chain} for dry-run`); }; /** @@ -74,12 +72,12 @@ export const impersonateAccount = async ( anvilIPAddr?: string, anvilPort?: number, ): Promise => { - logger.info(`Impersonating account (${address})...`); + rootLogger.info(`Impersonating account (${address})...`); const provider = getLocalProvider(anvilIPAddr, anvilPort); await provider.send(ANVIL_RPC_METHODS.IMPERSONATE_ACCOUNT, [address]); - logger.info(`✅ Successfully impersonated account (${address})`); + rootLogger.info(`✅ Successfully impersonated account (${address})`); return provider.getSigner(address); }; @@ -93,7 +91,7 @@ export const stopImpersonatingAccount = async ( anvilIPAddr?: string, anvilPort?: number, ) => { - logger.info(`Stopping account impersonation for address (${address})...`); + rootLogger.info(`Stopping account impersonation for address (${address})...`); if (isValidAddressEvm(address)) throw new Error( @@ -105,7 +103,7 @@ export const stopImpersonatingAccount = async ( address.substring(2), ]); - logger.info( + rootLogger.info( `✅ Successfully stopped account impersonation for address (${address})`, ); }; @@ -125,7 +123,7 @@ export const getLocalProvider = ( envUrl = `${ENDPOINT_PREFIX}${anvilIPAddr}:${anvilPort}`; if (urlOverride && !urlOverride.startsWith(ENDPOINT_PREFIX)) { - logger.warn( + rootLogger.warn( `⚠️ Provided URL override (${urlOverride}) does not begin with ${ENDPOINT_PREFIX}. Defaulting to ${ envUrl ?? DEFAULT_ANVIL_ENDPOINT }`, diff --git a/typescript/infra/src/utils/safe.ts b/typescript/sdk/src/utils/gnosisSafe.js similarity index 59% rename from typescript/infra/src/utils/safe.ts rename to typescript/sdk/src/utils/gnosisSafe.js index ae6343dde8..892785ec5d 100644 --- a/typescript/infra/src/utils/safe.ts +++ b/typescript/sdk/src/utils/gnosisSafe.js @@ -1,32 +1,19 @@ +// This file is JS because of https://github.com/safe-global/safe-core-sdk/issues/805 import SafeApiKit from '@safe-global/api-kit'; import Safe, { EthersAdapter } from '@safe-global/protocol-kit'; import { ethers } from 'ethers'; -import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk'; - -import { getChain } from '../../config/registry.js'; - -// NOTE about Safe: -// Accessing lib through .default due to https://github.com/safe-global/safe-core-sdk/issues/419 -// See also https://github.com/safe-global/safe-core-sdk/issues/514 - -export function getSafeService( - chain: ChainName, - multiProvider: MultiProvider, -): SafeApiKit.default { +export function getSafeService(chain, multiProvider) { const signer = multiProvider.getSigner(chain); const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }); - const txServiceUrl = getChain(chain).gnosisSafeTransactionServiceUrl; + const txServiceUrl = + multiProvider.getChainMetadata(chain).gnosisSafeTransactionServiceUrl; if (!txServiceUrl) throw new Error(`must provide tx service url for ${chain}`); return new SafeApiKit.default({ txServiceUrl, ethAdapter }); } -export function getSafe( - chain: ChainName, - multiProvider: MultiProvider, - safeAddress: string, -): Promise { +export function getSafe(chain, multiProvider, safeAddress) { const signer = multiProvider.getSigner(chain); const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }); return Safe.default.create({ @@ -35,20 +22,17 @@ export function getSafe( }); } -export async function getSafeDelegates( - service: SafeApiKit.default, - safeAddress: string, -) { +export async function getSafeDelegates(service, safeAddress) { const delegateResponse = await service.getSafeDelegates({ safeAddress }); return delegateResponse.results.map((r) => r.delegate); } export async function canProposeSafeTransactions( - proposer: string, - chain: ChainName, - multiProvider: MultiProvider, - safeAddress: string, -): Promise { + proposer, + chain, + multiProvider, + safeAddress, +) { let safeService; try { safeService = getSafeService(chain, multiProvider); diff --git a/typescript/utils/CHANGELOG.md b/typescript/utils/CHANGELOG.md index 55274e6232..2f6ec6c570 100644 --- a/typescript/utils/CHANGELOG.md +++ b/typescript/utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @hyperlane-xyz/utils +## 3.11.1 + +## 3.11.0 + +### Minor Changes + +- b63714ede: Convert all public hyperlane npm packages from CJS to pure ESM +- af2634207: Moved Hook/ISM config stringify into a general object stringify utility. + +### Patch Changes + +- 2b3f75836: Add objLength and isObjEmpty utils + ## 3.10.0 ### Minor Changes diff --git a/typescript/utils/package.json b/typescript/utils/package.json index c8754a615e..23b3801e73 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/utils", "description": "General utilities and types for the Hyperlane network", - "version": "3.10.0", + "version": "3.11.1", "dependencies": { "@cosmjs/encoding": "^0.31.3", "@solana/web3.js": "^1.78.0", diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index 70da60cac0..4fc2501234 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -13,8 +13,6 @@ export { capitalizeAddress, convertToProtocolAddress, ensure0x, - fromHexString, - toHexString, eqAddress, eqAddressCosmos, eqAddressEvm, @@ -121,6 +119,8 @@ export { streamToString, toTitleCase, trimToLength, + fromHexString, + toHexString, } from './strings.js'; export { isNullish, isNumeric } from './typeof.js'; export { diff --git a/typescript/utils/src/strings.ts b/typescript/utils/src/strings.ts index 3690ace198..26d40838aa 100644 --- a/typescript/utils/src/strings.ts +++ b/typescript/utils/src/strings.ts @@ -1,3 +1,5 @@ +import { ensure0x, strip0x } from './addresses.js'; + export function toTitleCase(str: string) { return str.replace(/\w\S*/g, (txt) => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); @@ -38,3 +40,8 @@ export function errorToString(error: any, maxLength = 300) { if (typeof details === 'string') return trimToLength(details, maxLength); return trimToLength(JSON.stringify(details), maxLength); } + +export const fromHexString = (hexstr: string) => + Buffer.from(strip0x(hexstr), 'hex'); + +export const toHexString = (buf: Buffer) => ensure0x(buf.toString('hex')); diff --git a/yarn.lock b/yarn.lock index 1477af70a4..9cfee7cc2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4955,8 +4955,8 @@ __metadata: resolution: "@hyperlane-xyz/cli@workspace:typescript/cli" dependencies: "@hyperlane-xyz/registry": "npm:^1.0.7" - "@hyperlane-xyz/sdk": "npm:3.10.0" - "@hyperlane-xyz/utils": "npm:3.10.0" + "@hyperlane-xyz/sdk": "npm:3.11.1" + "@hyperlane-xyz/utils": "npm:3.11.1" "@inquirer/prompts": "npm:^3.0.0" "@types/mocha": "npm:^10.0.1" "@types/node": "npm:^18.14.5" @@ -4983,12 +4983,12 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:3.10.0, @hyperlane-xyz/core@workspace:solidity": +"@hyperlane-xyz/core@npm:3.11.1, @hyperlane-xyz/core@workspace:solidity": version: 0.0.0-use.local resolution: "@hyperlane-xyz/core@workspace:solidity" dependencies: "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:3.10.0" + "@hyperlane-xyz/utils": "npm:3.11.1" "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" "@layerzerolabs/solidity-examples": "npm:^1.1.0" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" @@ -5000,8 +5000,10 @@ __metadata: chai: "npm:^4.3.6" ethereum-waffle: "npm:^4.0.10" ethers: "npm:^5.7.2" + fx-portal: "npm:^1.0.3" hardhat: "npm:^2.22.2" hardhat-gas-reporter: "npm:^1.0.9" + hardhat-ignore-warnings: "npm:^0.2.11" prettier: "npm:^2.8.8" prettier-plugin-solidity: "npm:^1.1.3" solhint: "npm:^4.5.4" @@ -5034,13 +5036,13 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/helloworld@npm:3.10.0, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": +"@hyperlane-xyz/helloworld@npm:3.11.1, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": version: 0.0.0-use.local resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld" dependencies: - "@hyperlane-xyz/core": "npm:3.10.0" + "@hyperlane-xyz/core": "npm:3.11.1" "@hyperlane-xyz/registry": "npm:^1.0.7" - "@hyperlane-xyz/sdk": "npm:3.10.0" + "@hyperlane-xyz/sdk": "npm:3.11.1" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" @@ -5085,15 +5087,13 @@ __metadata: "@ethersproject/experimental": "npm:^5.7.0" "@ethersproject/hardware-wallets": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.2" - "@hyperlane-xyz/helloworld": "npm:3.10.0" + "@hyperlane-xyz/helloworld": "npm:3.11.1" "@hyperlane-xyz/registry": "npm:^1.0.7" - "@hyperlane-xyz/sdk": "npm:3.10.0" - "@hyperlane-xyz/utils": "npm:3.10.0" + "@hyperlane-xyz/sdk": "npm:3.11.1" + "@hyperlane-xyz/utils": "npm:3.11.1" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-etherscan": "npm:^3.0.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" - "@safe-global/api-kit": "npm:^1.3.0" - "@safe-global/protocol-kit": "npm:^1.2.0" "@solana/web3.js": "npm:^1.78.0" "@types/chai": "npm:^4.2.21" "@types/json-stable-stringify": "npm:^1.0.36" @@ -5150,17 +5150,19 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/sdk@npm:3.10.0, @hyperlane-xyz/sdk@workspace:typescript/sdk": +"@hyperlane-xyz/sdk@npm:3.11.1, @hyperlane-xyz/sdk@workspace:typescript/sdk": version: 0.0.0-use.local resolution: "@hyperlane-xyz/sdk@workspace:typescript/sdk" dependencies: "@aws-sdk/client-s3": "npm:^3.74.0" "@cosmjs/cosmwasm-stargate": "npm:^0.31.3" "@cosmjs/stargate": "npm:^0.31.3" - "@hyperlane-xyz/core": "npm:3.10.0" - "@hyperlane-xyz/utils": "npm:3.10.0" + "@hyperlane-xyz/core": "npm:3.11.1" + "@hyperlane-xyz/utils": "npm:3.11.1" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" + "@safe-global/api-kit": "npm:1.3.0" + "@safe-global/protocol-kit": "npm:1.3.0" "@solana/spl-token": "npm:^0.3.8" "@solana/web3.js": "npm:^1.78.0" "@types/coingecko-api": "npm:^1.0.10" @@ -5224,7 +5226,7 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/utils@npm:3.10.0, @hyperlane-xyz/utils@workspace:typescript/utils": +"@hyperlane-xyz/utils@npm:3.11.1, @hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" dependencies: @@ -6844,6 +6846,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts@npm:^4.2.0": + version: 4.9.6 + resolution: "@openzeppelin/contracts@npm:4.9.6" + checksum: 71f45ad42e68c0559be4ba502115462a01c76fc805c08d3005c10b5550a093f1a2b00b2d7e9d6d1f331e147c50fd4ad832f71c4470ec5b34f5a2d0751cd19a47 + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:^4.4.1": version: 4.9.5 resolution: "@openzeppelin/contracts@npm:4.9.5" @@ -7235,7 +7244,7 @@ __metadata: languageName: node linkType: hard -"@safe-global/api-kit@npm:^1.3.0": +"@safe-global/api-kit@npm:1.3.0": version: 1.3.0 resolution: "@safe-global/api-kit@npm:1.3.0" dependencies: @@ -7246,9 +7255,9 @@ __metadata: languageName: node linkType: hard -"@safe-global/protocol-kit@npm:^1.2.0": - version: 1.2.0 - resolution: "@safe-global/protocol-kit@npm:1.2.0" +"@safe-global/protocol-kit@npm:1.3.0": + version: 1.3.0 + resolution: "@safe-global/protocol-kit@npm:1.3.0" dependencies: "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" @@ -7259,7 +7268,8 @@ __metadata: web3: "npm:^1.8.1" web3-core: "npm:^1.8.1" web3-utils: "npm:^1.8.1" - checksum: f6c6969ee5638fc1af1bf56f7848915bb9cb1e6a3f57f4e9b235f3f251c3a5f8cde6bad13bd8b9a321424cdecdad34e02ad13de3b4fe8fae32acc9000e52b4dc + zksync-web3: "npm:^0.14.3" + checksum: e562f437c3682ddf395e13b26adb9f4e4d2970c66b78e8f8f4895862864ac5bdfac3bdcfda234a171a3eb79d262b75d48cac3ff248f4587654b7b8da9a1ba7f6 languageName: node linkType: hard @@ -14278,6 +14288,15 @@ __metadata: languageName: node linkType: hard +"fx-portal@npm:^1.0.3": + version: 1.0.3 + resolution: "fx-portal@npm:1.0.3" + dependencies: + "@openzeppelin/contracts": "npm:^4.2.0" + checksum: 89309e03da57238d153b41fd9fd492d582d41a90da51fc18b4cdd939a8713736572ed1ba034210888ad1b2e81596a860f157785f6911e6d265e2fd0730aa94c2 + languageName: node + linkType: hard + "ganache@npm:7.4.3": version: 7.4.3 resolution: "ganache@npm:7.4.3" @@ -14964,6 +14983,17 @@ __metadata: languageName: node linkType: hard +"hardhat-ignore-warnings@npm:^0.2.11": + version: 0.2.11 + resolution: "hardhat-ignore-warnings@npm:0.2.11" + dependencies: + minimatch: "npm:^5.1.0" + node-interval-tree: "npm:^2.0.1" + solidity-comments: "npm:^0.0.2" + checksum: b249e02dbc207a40cb3090577c0f972b52f233b062ebafed413e70454ad389f991f32a8fda582f32d87f6057886e529a9f58ff6abb604d3294f7f4697a6dcb16 + languageName: node + linkType: hard + "hardhat@npm:^2.22.2": version: 2.22.2 resolution: "hardhat@npm:2.22.2" @@ -18087,6 +18117,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^5.1.0": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 + languageName: node + linkType: hard + "minimist-options@npm:^4.0.2": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -18876,6 +18915,15 @@ __metadata: languageName: node linkType: hard +"node-interval-tree@npm:^2.0.1": + version: 2.1.2 + resolution: "node-interval-tree@npm:2.1.2" + dependencies: + shallowequal: "npm:^1.1.0" + checksum: da3b54b12720fa20a939a24dfb890b54f29c1c939a837e89d6197cbb96701f65279786a05843ac99ded60e8362cc48fda4ac2466a2e65a34b3fb84b648487d9f + languageName: node + linkType: hard + "node-releases@npm:^2.0.14": version: 2.0.14 resolution: "node-releases@npm:2.0.14" @@ -21366,6 +21414,13 @@ __metadata: languageName: node linkType: hard +"shallowequal@npm:^1.1.0": + version: 1.1.0 + resolution: "shallowequal@npm:1.1.0" + checksum: f4c1de0837f106d2dbbfd5d0720a5d059d1c66b42b580965c8f06bb1db684be8783538b684092648c981294bf817869f743a066538771dbecb293df78f765e00 + languageName: node + linkType: hard + "shebang-command@npm:^1.2.0": version: 1.2.0 resolution: "shebang-command@npm:1.2.0" @@ -21667,6 +21722,20 @@ __metadata: languageName: node linkType: hard +"solidity-comments-darwin-arm64@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-darwin-arm64@npm:0.0.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"solidity-comments-darwin-x64@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-darwin-x64@npm:0.0.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "solidity-comments-extractor@npm:^0.0.7": version: 0.0.7 resolution: "solidity-comments-extractor@npm:0.0.7" @@ -21674,6 +21743,101 @@ __metadata: languageName: node linkType: hard +"solidity-comments-freebsd-x64@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-freebsd-x64@npm:0.0.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"solidity-comments-linux-arm64-gnu@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-arm64-gnu@npm:0.0.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"solidity-comments-linux-arm64-musl@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-arm64-musl@npm:0.0.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"solidity-comments-linux-x64-gnu@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-x64-gnu@npm:0.0.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"solidity-comments-linux-x64-musl@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-x64-musl@npm:0.0.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"solidity-comments-win32-arm64-msvc@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-win32-arm64-msvc@npm:0.0.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"solidity-comments-win32-ia32-msvc@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-win32-ia32-msvc@npm:0.0.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"solidity-comments-win32-x64-msvc@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-win32-x64-msvc@npm:0.0.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"solidity-comments@npm:^0.0.2": + version: 0.0.2 + resolution: "solidity-comments@npm:0.0.2" + dependencies: + solidity-comments-darwin-arm64: "npm:0.0.2" + solidity-comments-darwin-x64: "npm:0.0.2" + solidity-comments-freebsd-x64: "npm:0.0.2" + solidity-comments-linux-arm64-gnu: "npm:0.0.2" + solidity-comments-linux-arm64-musl: "npm:0.0.2" + solidity-comments-linux-x64-gnu: "npm:0.0.2" + solidity-comments-linux-x64-musl: "npm:0.0.2" + solidity-comments-win32-arm64-msvc: "npm:0.0.2" + solidity-comments-win32-ia32-msvc: "npm:0.0.2" + solidity-comments-win32-x64-msvc: "npm:0.0.2" + dependenciesMeta: + solidity-comments-darwin-arm64: + optional: true + solidity-comments-darwin-x64: + optional: true + solidity-comments-freebsd-x64: + optional: true + solidity-comments-linux-arm64-gnu: + optional: true + solidity-comments-linux-arm64-musl: + optional: true + solidity-comments-linux-x64-gnu: + optional: true + solidity-comments-linux-x64-musl: + optional: true + solidity-comments-win32-arm64-msvc: + optional: true + solidity-comments-win32-ia32-msvc: + optional: true + solidity-comments-win32-x64-msvc: + optional: true + checksum: a39b0340c964f2a13594bc34c36656072abfb3cac459f6ca3611aedbbb2ff82d2821e99bfa2cff083af10409b49396c844e55b13acde81ba2758363fa82a6ea8 + languageName: node + linkType: hard + "solidity-coverage@npm:^0.8.3": version: 0.8.3 resolution: "solidity-coverage@npm:0.8.3" @@ -24673,6 +24837,15 @@ __metadata: languageName: node linkType: hard +"zksync-web3@npm:^0.14.3": + version: 0.14.4 + resolution: "zksync-web3@npm:0.14.4" + peerDependencies: + ethers: ^5.7.0 + checksum: a1566a2a2ba34a3026680f3b4000ffa02593e02d9c73a4dd143bde929b5e39b09544d429bccad0479070670cfdad5f6836cb686c4b8d7954b4d930826be91c92 + languageName: node + linkType: hard + "zod@npm:^3.21.2": version: 3.21.2 resolution: "zod@npm:3.21.2"