From 8b5421194ce17860d8b5c82af40433ba8c17e22a Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 21 Jan 2025 11:19:59 +0000 Subject: [PATCH 1/5] feat: modify gas oracle TS tooling to be friendly for all protocol types (#5198) ### Description - Part of a greater effort to consolidate gas oracle configuration - Adds a script `./scripts/sealevel-helpers/print-gas-oracles.ts` that outputs all SVM gas oracles that can be plugged into SVM tooling (opening a separate PR for the SVM tooling bits) - Attempts to simplify and consolidate tooling for generating gas oracle configuration. We no longer have duped logic in infra, and instead impose min USD costs by still using an SDK function. The SDK function now generates gas oracle configurations that are specific to the origin smart contract requirements - Doing a fork test shows that the quotes are the exact same as before ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --------- Co-authored-by: Paul Balaji <10051819+paulbalaji@users.noreply.github.com> Co-authored-by: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> --- rust/main/utils/run-locally/src/solana.rs | 78 +++-- rust/sealevel/client/src/core.rs | 94 +---- rust/sealevel/client/src/igp.rs | 329 ++++++++++++------ rust/sealevel/client/src/main.rs | 23 +- .../local-e2e/gas-oracle-configs.json | 34 +- .../environments/local-e2e/overheads.json | 10 - .../environments/mainnet3/chain-config.json | 275 ++++++++++++++- .../gas-oracle-configs-eclipsemainnet.json | 29 -- .../mainnet3/gas-oracle-configs.json | 136 ++++++++ .../gas-oracle-configs-solanamainnet.json | 20 -- .../soon/gas-oracle-configs-soon.json | 20 -- .../testnet4/gas-oracle-configs.json | 58 ++- typescript/cli/src/config/hooks.ts | 1 + .../infra/config/environments/mainnet3/igp.ts | 21 +- .../config/environments/test/gas-oracle.ts | 31 +- .../environments/testnet4/gasPrices.json | 4 +- .../infra/config/environments/testnet4/igp.ts | 23 +- .../environments/testnet4/tokenPrices.json | 1 + typescript/infra/config/registry.ts | 4 +- typescript/infra/scripts/agent-utils.ts | 7 + .../sealevel-helpers/print-gas-oracles.ts | 140 ++++++++ .../print-multisig-ism-config.ts | 2 - .../warp-routes/generate-warp-config.ts | 14 +- typescript/infra/src/config/gas-oracle.ts | 260 ++++++++------ typescript/sdk/src/consts/igp.ts | 47 ++- .../sdk/src/gas/HyperlaneIgpDeployer.ts | 10 +- .../configure-gas-oracles.hardhat-test.ts | 1 + typescript/sdk/src/gas/oracle/types.ts | 22 +- typescript/sdk/src/gas/utils.ts | 190 ++++++---- .../src/hook/EvmHookModule.hardhat-test.ts | 16 + typescript/sdk/src/hook/EvmHookModule.ts | 4 +- typescript/sdk/src/hook/EvmHookReader.ts | 4 +- typescript/sdk/src/hook/types.ts | 4 +- typescript/sdk/src/index.ts | 9 +- typescript/sdk/src/test/testUtils.ts | 1 + typescript/sdk/src/warp/WarpCore.ts | 6 +- typescript/utils/src/amount.ts | 20 +- typescript/utils/src/index.ts | 1 + 38 files changed, 1330 insertions(+), 619 deletions(-) delete mode 100644 rust/sealevel/environments/local-e2e/overheads.json delete mode 100644 rust/sealevel/environments/mainnet3/eclipsemainnet/gas-oracle-configs-eclipsemainnet.json create mode 100644 rust/sealevel/environments/mainnet3/gas-oracle-configs.json delete mode 100644 rust/sealevel/environments/mainnet3/solanamainnet/gas-oracle-configs-solanamainnet.json delete mode 100644 rust/sealevel/environments/mainnet3/soon/gas-oracle-configs-soon.json create mode 100644 typescript/infra/scripts/sealevel-helpers/print-gas-oracles.ts diff --git a/rust/main/utils/run-locally/src/solana.rs b/rust/main/utils/run-locally/src/solana.rs index 3e32524cdd..fdb315f0b8 100644 --- a/rust/main/utils/run-locally/src/solana.rs +++ b/rust/main/utils/run-locally/src/solana.rs @@ -69,13 +69,15 @@ const SBF_OUT_PATH: &str = "target/dist"; const SOLANA_LOCAL_CHAIN_ID: &str = "13375"; const SOLANA_REMOTE_CHAIN_ID: &str = "13376"; +const SEALEVELTEST1_IGP_PROGRAM_ID: &str = "GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U"; +const SEALEVELTEST2_IGP_PROGRAM_ID: &str = "FArd4tEikwz2fk3MB7S9kC82NGhkgT6f9aXi3C5cw1E5"; + // TODO: use a temp dir instead! pub const SOLANA_CHECKPOINT_LOCATION: &str = "/tmp/test_sealevel_checkpoints_0x70997970c51812dc3a010c7d01b50e0d17dc79c8"; const SOLANA_GAS_ORACLE_CONFIG_FILE: &str = "../sealevel/environments/local-e2e/gas-oracle-configs.json"; -const SOLANA_OVERHEAD_CONFIG_FILE: &str = "../sealevel/environments/local-e2e/overheads.json"; // Install the CLI tools and return the path to the bin dir. #[apply(as_task)] @@ -233,23 +235,41 @@ pub fn start_solana_test_validator( .cmd("deploy") .arg("environment", SOLANA_ENV_NAME) .arg("environments-dir", SOLANA_ENVS_DIR) - .arg("built-so-dir", SBF_OUT_PATH) - .arg("overhead-config-file", SOLANA_OVERHEAD_CONFIG_FILE); + .arg("built-so-dir", SBF_OUT_PATH); + // Deploy sealeveltest1 core sealevel_client_deploy_core .clone() .arg("local-domain", SOLANA_LOCAL_CHAIN_ID) - .arg( - "remote-domains", - [SOLANA_REMOTE_CHAIN_ID, "9913371", "9913372", "9913373"].join(","), - ) .arg("chain", "sealeveltest1") .run() .join(); + // Deploy sealeveltest2 core sealevel_client_deploy_core .arg("local-domain", SOLANA_REMOTE_CHAIN_ID) - .arg("remote-domains", SOLANA_LOCAL_CHAIN_ID) + .arg("chain", "sealeveltest2") + .run() + .join(); + + let igp_configure_command = sealevel_client + .clone() + .cmd("igp") + .cmd("configure") + .arg("gas-oracle-config-file", SOLANA_GAS_ORACLE_CONFIG_FILE) + .arg("chain-config-file", SOLANA_CHAIN_CONFIG_FILE); + + // Configure sealeveltest1 IGP + igp_configure_command + .clone() + .arg("program-id", SEALEVELTEST1_IGP_PROGRAM_ID) + .arg("chain", "sealeveltest1") + .run() + .join(); + + // Configure sealeveltest2 IGP + igp_configure_command + .arg("program-id", SEALEVELTEST2_IGP_PROGRAM_ID) .arg("chain", "sealeveltest2") .run() .join(); @@ -294,40 +314,44 @@ pub fn start_solana_test_validator( .run() .join(); + // So we can test paying for gas with a different IGP account + const ALTERNATIVE_SALT: &str = + "0x0000000000000000000000000000000000000000000000000000000000000001"; + const ALTERNATIVE_IGP_ACCOUNT: &str = "8EniU8dQaGQ3HWWtT77V7hrksheygvEu6TtzJ3pX1nKM"; + sealevel_client .clone() .cmd("igp") .cmd("init-igp-account") - .arg("program-id", "GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U") + .arg("program-id", SEALEVELTEST1_IGP_PROGRAM_ID) .arg("environment", SOLANA_ENV_NAME) .arg("environments-dir", SOLANA_ENVS_DIR) .arg("chain", "sealeveltest1") - .arg("chain-config-file", SOLANA_CHAIN_CONFIG_FILE) - .arg("gas-oracle-config-file", SOLANA_GAS_ORACLE_CONFIG_FILE) - .arg( - "account-salt", - "0x0000000000000000000000000000000000000000000000000000000000000001", - ) + .arg("account-salt", ALTERNATIVE_SALT) .run() .join(); sealevel_client + .clone() .cmd("igp") .cmd("init-overhead-igp-account") - .arg("program-id", "GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U") + .arg("program-id", SEALEVELTEST1_IGP_PROGRAM_ID) .arg("environment", SOLANA_ENV_NAME) .arg("environments-dir", SOLANA_ENVS_DIR) .arg("chain", "sealeveltest1") + .arg("inner-igp-account", ALTERNATIVE_IGP_ACCOUNT) + .arg("account-salt", ALTERNATIVE_SALT) + .run() + .join(); + + sealevel_client + .cmd("igp") + .cmd("configure") + .arg("program-id", SEALEVELTEST1_IGP_PROGRAM_ID) + .arg("gas-oracle-config-file", SOLANA_GAS_ORACLE_CONFIG_FILE) .arg("chain-config-file", SOLANA_CHAIN_CONFIG_FILE) - .arg("overhead-config-file", SOLANA_OVERHEAD_CONFIG_FILE) - .arg( - "inner-igp-account", - "8EniU8dQaGQ3HWWtT77V7hrksheygvEu6TtzJ3pX1nKM", - ) - .arg( - "account-salt", - "0x0000000000000000000000000000000000000000000000000000000000000001", - ) + .arg("chain", "sealeveltest1") + .arg("account-salt", ALTERNATIVE_SALT) .run() .join(); @@ -372,7 +396,7 @@ pub fn initiate_solana_hyperlane_transfer( sealevel_client(&solana_cli_tools_path, &solana_config_path) .cmd("igp") .cmd("pay-for-gas") - .arg("program-id", "GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U") + .arg("program-id", SEALEVELTEST1_IGP_PROGRAM_ID) .arg("message-id", message_id.clone()) .arg("destination-domain", SOLANA_REMOTE_CHAIN_ID) .arg("gas", "100000") @@ -419,7 +443,7 @@ pub fn initiate_solana_non_matching_igp_paying_transfer( sealevel_client(&solana_cli_tools_path, &solana_config_path) .cmd("igp") .cmd("pay-for-gas") - .arg("program-id", "GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U") + .arg("program-id", SEALEVELTEST1_IGP_PROGRAM_ID) .arg("message-id", non_matching_igp_message_id.clone()) .arg("destination-domain", SOLANA_REMOTE_CHAIN_ID) .arg("gas", "100000") diff --git a/rust/sealevel/client/src/core.rs b/rust/sealevel/client/src/core.rs index 72ecfcb5ca..534b597965 100644 --- a/rust/sealevel/client/src/core.rs +++ b/rust/sealevel/client/src/core.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use solana_program::pubkey::Pubkey; use solana_sdk::{compute_budget, compute_budget::ComputeBudgetInstruction}; -use std::collections::HashMap; use std::{fs::File, path::Path}; use crate::cmd_utils::get_compute_unit_price_micro_lamports_for_chain_name; @@ -17,7 +16,6 @@ use crate::{ Context, CoreCmd, CoreDeploy, CoreSubCmd, }; use hyperlane_core::H256; -use hyperlane_sealevel_igp::accounts::{SOL_DECIMALS, TOKEN_EXCHANGE_RATE_SCALE}; pub(crate) fn adjust_gas_price_if_needed(chain_name: &str, ctx: &mut Context) { if chain_name.eq("solanamainnet") { @@ -206,56 +204,9 @@ fn deploy_validator_announce( program_id } -#[allow(clippy::too_many_arguments)] +/// Deploys the IGP program and initializes the zero salt IGP and overhead IGP accounts. +/// Configuration of gas oracles is expected to be done separately. fn deploy_igp(ctx: &mut Context, core: &CoreDeploy, key_dir: &Path) -> (Pubkey, Pubkey, Pubkey) { - use hyperlane_sealevel_igp::{ - accounts::{GasOracle, RemoteGasData}, - instruction::{GasOracleConfig, GasOverheadConfig}, - }; - - let mut gas_oracle_configs = core - .gas_oracle_config_file - .as_deref() - .map(|p| { - let file = File::open(p).expect("Failed to open oracle config file"); - serde_json::from_reader::<_, Vec>(file) - .expect("Failed to parse oracle config file") - }) - .unwrap_or_default() - .into_iter() - .filter(|c| c.domain != core.local_domain) - .map(|c| (c.domain, c)) - .collect::>(); - for &remote in &core.remote_domains { - gas_oracle_configs - .entry(remote) - .or_insert_with(|| GasOracleConfig { - domain: remote, - gas_oracle: Some(GasOracle::RemoteGasData(RemoteGasData { - token_exchange_rate: TOKEN_EXCHANGE_RATE_SCALE, - gas_price: 1, - token_decimals: SOL_DECIMALS, - })), - }); - } - let gas_oracle_configs = gas_oracle_configs.into_values().collect::>(); - - let overhead_configs = core - .overhead_config_file - .as_deref() - .map(|p| { - let file = File::open(p).expect("Failed to open overhead config file"); - serde_json::from_reader::<_, Vec>(file) - .expect("Failed to parse overhead config file") - }) - .unwrap_or_default() - .into_iter() - .filter(|c| c.destination_domain != core.local_domain) - .map(|c| (c.destination_domain, c)) - .collect::>() // dedup - .into_values() - .collect::>(); - let program_id = deploy_program( ctx.payer_keypair_path(), key_dir, @@ -319,47 +270,6 @@ fn deploy_igp(ctx: &mut Context, core: &CoreDeploy, key_dir: &Path) -> (Pubkey, println!("Initialized overhead IGP account {}", overhead_igp_account); - if !gas_oracle_configs.is_empty() { - let domains = gas_oracle_configs - .iter() - .map(|c| c.domain) - .collect::>(); - let instruction = hyperlane_sealevel_igp::instruction::set_gas_oracle_configs_instruction( - program_id, - igp_account, - ctx.payer_pubkey, - gas_oracle_configs, - ) - .unwrap(); - - ctx.new_txn().add(instruction).send_with_payer(); - - println!("Set gas oracle for remote domains {domains:?}",); - } else { - println!("Skipping settings gas oracle config"); - } - - if !overhead_configs.is_empty() { - let domains = overhead_configs - .iter() - .map(|c| c.destination_domain) - .collect::>(); - - let instruction = hyperlane_sealevel_igp::instruction::set_destination_gas_overheads( - program_id, - overhead_igp_account, - ctx.payer_pubkey, - overhead_configs, - ) - .unwrap(); - - ctx.new_txn().add(instruction).send_with_payer(); - - println!("Set gas overheads for remote domains {domains:?}",) - } else { - println!("Skipping setting gas overheads"); - } - (program_id, overhead_igp_account, igp_account) } diff --git a/rust/sealevel/client/src/igp.rs b/rust/sealevel/client/src/igp.rs index 60f51479d9..8f898d6749 100644 --- a/rust/sealevel/client/src/igp.rs +++ b/rust/sealevel/client/src/igp.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::{ artifacts::{read_json, try_read_json, write_json, SingularProgramIdArtifact}, @@ -9,11 +9,7 @@ use crate::{ Context, GasOverheadSubCmd, GetSetCmd, IgpCmd, IgpSubCmd, }; -use std::{ - fs::File, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{path::Path, str::FromStr}; use solana_sdk::{ pubkey::Pubkey, @@ -31,6 +27,14 @@ use hyperlane_sealevel_igp::{ instruction::{GasOracleConfig, GasOverheadConfig}, }; +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +/// Compatible with the format of our TS-generated configs. +struct GasOracleConfigWithOverhead { + oracle_config: RemoteGasData, + overhead: Option, +} + #[derive(Debug, Serialize, Deserialize, Default)] struct IgpAccountsArtifacts { salt: H256, @@ -109,16 +113,7 @@ pub(crate) fn process_igp_cmd(mut ctx: Context, cmd: IgpCmd) { }) .unwrap_or_else(|| get_context_salt(init.context.as_ref())); - let chain_configs = - read_json::>(&init.chain_config_file); - - let igp_account = init_and_configure_igp_account( - &mut ctx, - init.program_id, - chain_configs.get(&init.chain).unwrap().domain_id(), - salt, - init.gas_oracle_config_file, - ); + let igp_account = init_igp_account(&mut ctx, init.program_id, salt); let artifacts = IgpAccountsArtifacts { salt, @@ -155,17 +150,8 @@ pub(crate) fn process_igp_cmd(mut ctx: Context, cmd: IgpCmd) { }) .unwrap_or_else(|| get_context_salt(init.context.as_ref())); - let chain_configs = - read_json::>(&init.chain_config_file); - - let overhead_igp_account = init_and_configure_overhead_igp_account( - &mut ctx, - init.program_id, - init.inner_igp_account, - chain_configs.get(&init.chain).unwrap().domain_id(), - salt, - init.overhead_config_file, - ); + let overhead_igp_account = + init_overhead_igp_account(&mut ctx, init.program_id, init.inner_igp_account, salt); let artifacts = IgpAccountsArtifacts { salt, @@ -440,6 +426,16 @@ pub(crate) fn process_igp_cmd(mut ctx: Context, cmd: IgpCmd) { ) .send_with_payer(); } + IgpSubCmd::Configure(args) => { + configure_igp_and_overhead_igp( + &mut ctx, + args.program_id, + args.chain, + &args.gas_oracle_config_file, + &args.chain_config_file, + args.account_salt, + ); + } } } @@ -485,25 +481,7 @@ fn deploy_igp_program( program_id } -fn init_and_configure_igp_account( - ctx: &mut Context, - program_id: Pubkey, - local_domain: u32, - salt: H256, - gas_oracle_config_file: Option, -) -> Pubkey { - let gas_oracle_configs = gas_oracle_config_file - .as_deref() - .map(|p| { - let file = File::open(p).expect("Failed to open oracle config file"); - serde_json::from_reader::<_, Vec>(file) - .expect("Failed to parse oracle config file") - }) - .unwrap_or_default() - .into_iter() - .filter(|c| c.domain != local_domain) - .collect::>(); - +fn init_igp_account(ctx: &mut Context, program_id: Pubkey, salt: H256) -> Pubkey { // Initialize IGP with the given salt let (igp_account_pda, _igp_account_bump) = Pubkey::find_program_address(hyperlane_sealevel_igp::igp_pda_seeds!(salt), &program_id); @@ -537,54 +515,15 @@ fn init_and_configure_igp_account( ); } - if !gas_oracle_configs.is_empty() { - // TODO: idempotency - - let domains = gas_oracle_configs - .iter() - .map(|c| c.domain) - .collect::>(); - let instruction = hyperlane_sealevel_igp::instruction::set_gas_oracle_configs_instruction( - program_id, - igp_account_pda, - ctx.payer_pubkey, - gas_oracle_configs, - ) - .unwrap(); - - ctx.new_txn().add(instruction).send_with_payer(); - - println!("Set gas oracle for remote domains {domains:?}",); - } else { - println!("Skipping settings gas oracle config"); - } - igp_account_pda } -fn init_and_configure_overhead_igp_account( +fn init_overhead_igp_account( ctx: &mut Context, program_id: Pubkey, inner_igp_account: Pubkey, - local_domain: u32, salt: H256, - overhead_config_file: Option, ) -> Pubkey { - let overhead_configs = overhead_config_file - .as_deref() - .map(|p| { - let file = File::open(p).expect("Failed to open overhead config file"); - serde_json::from_reader::<_, Vec>(file) - .expect("Failed to parse overhead config file") - }) - .unwrap_or_default() - .into_iter() - .filter(|c| c.destination_domain != local_domain) - .map(|c| (c.destination_domain, c)) - .collect::>() // dedup - .into_values() - .collect::>(); - let (overhead_igp_account, _) = Pubkey::find_program_address( hyperlane_sealevel_igp::overhead_igp_pda_seeds!(salt), &program_id, @@ -619,28 +558,210 @@ fn init_and_configure_overhead_igp_account( ); } - if !overhead_configs.is_empty() { - // TODO: idempotency + overhead_igp_account +} - let domains = overhead_configs - .iter() - .map(|c| c.destination_domain) - .collect::>(); +/// Idempotently applies gas oracles to the IGP account and overheads to the Overhead IGP +/// account relating to the IGP account salt. +fn configure_igp_and_overhead_igp( + ctx: &mut Context, + program_id: Pubkey, + local_chain: String, + gas_oracle_config_file: &Path, + chain_config_path: &Path, + account_salt: Option, +) { + let chain_configs = read_json::>(chain_config_path); - let instruction = hyperlane_sealevel_igp::instruction::set_destination_gas_overheads( - program_id, - overhead_igp_account, - ctx.payer_pubkey, - overhead_configs, - ) - .unwrap(); + let gas_oracle_configs = read_json::< + HashMap>, + >(gas_oracle_config_file); + let gas_oracle_config = gas_oracle_configs.get(&local_chain).unwrap(); - ctx.new_txn().add(instruction).send_with_payer(); + let salt = account_salt.unwrap_or_else(H256::zero); - println!("Set gas overheads for remote domains {domains:?}",) - } else { - println!("Skipping setting gas overheads"); + let (igp_account_pubkey, _bump) = + Pubkey::find_program_address(hyperlane_sealevel_igp::igp_pda_seeds!(salt), &program_id); + let igp_account = ctx + .client + .get_account_with_commitment(&igp_account_pubkey, ctx.commitment) + .unwrap() + .value + .expect("IGP account not found. Make sure you are connected to the right RPC."); + let igp_account = IgpAccount::fetch(&mut &igp_account.data[..]) + .unwrap() + .into_inner(); + + let (overhead_igp_account_pubkey, _bump) = Pubkey::find_program_address( + hyperlane_sealevel_igp::overhead_igp_pda_seeds!(salt), + &program_id, + ); + let overhead_igp_account = ctx + .client + .get_account_with_commitment(&overhead_igp_account_pubkey, ctx.commitment) + .unwrap() + .value + .expect("Overhead IGP account not found. Make sure you are connected to the right RPC."); + let overhead_igp_account = OverheadIgpAccount::fetch(&mut &overhead_igp_account.data[..]) + .unwrap() + .into_inner(); + + // Set IGP configurations + println!( + "Setting IGP configurations for IGP account {} and overhead IGP account {}", + igp_account_pubkey, overhead_igp_account_pubkey + ); + + let all_config_domain_ids = gas_oracle_config + .iter() + .map(|(remote, _)| chain_configs.get(remote).unwrap().domain_id()) + .collect::>(); + + // Remove any gas oracles not in the config + for (remote_domain, _) in igp_account.gas_oracles.iter() { + if !all_config_domain_ids.contains(remote_domain) { + let gas_oracle_config = GasOracleConfig { + domain: *remote_domain, + gas_oracle: None, + }; + println!( + "Removing oracle for remote domain {:?} that is not in the config", + remote_domain + ); + // For simplicity and to always be well within max tx sizes, just send one config at a time + let instruction = + hyperlane_sealevel_igp::instruction::set_gas_oracle_configs_instruction( + program_id, + igp_account_pubkey, + ctx.payer_pubkey, + vec![gas_oracle_config], + ) + .unwrap(); + + ctx.new_txn().add(instruction).send_with_payer(); + + println!("Removed gas oracle for remote domain {:?}", remote_domain); + } } - overhead_igp_account + // Remove any gas overheads not in the config + for (remote_domain, _) in overhead_igp_account.gas_overheads.iter() { + if !all_config_domain_ids.contains(remote_domain) { + let overhead_config = GasOverheadConfig { + destination_domain: *remote_domain, + gas_overhead: None, + }; + println!( + "Removing overhead for remote domain {:?} that is not in the config", + remote_domain + ); + // For simplicity and to always be well within max tx sizes, just send one config at a time + let instruction = hyperlane_sealevel_igp::instruction::set_destination_gas_overheads( + program_id, + overhead_igp_account_pubkey, + ctx.payer_pubkey, + vec![overhead_config], + ) + .unwrap(); + + ctx.new_txn().add(instruction).send_with_payer(); + + println!("Removed gas overhead for remote domain {:?}", remote_domain); + } + } + + // Make sure the gas oracles and overheads are set correctly + for (remote, config) in gas_oracle_config.iter() { + let remote_domain = chain_configs.get(remote).unwrap().domain_id(); + let gas_oracle_config = GasOracleConfig { + domain: remote_domain, + gas_oracle: Some(GasOracle::RemoteGasData(config.oracle_config.clone())), + }; + + // Gas oracle on the IGP account + if !map_configuration_matches( + &igp_account.gas_oracles, + remote, + remote_domain, + gas_oracle_config.gas_oracle.as_ref(), + ) { + println!( + "Setting gas oracle for remote domain {:?} ({:?}) with config {:?}", + remote, remote_domain, gas_oracle_config + ); + // For simplicity and to always be well within max tx sizes, just send one config at a time + let instruction = + hyperlane_sealevel_igp::instruction::set_gas_oracle_configs_instruction( + program_id, + igp_account_pubkey, + ctx.payer_pubkey, + vec![gas_oracle_config], + ) + .unwrap(); + + ctx.new_txn().add(instruction).send_with_payer(); + + println!( + "Set gas oracle for remote domain {:?} ({:?})", + remote, remote_domain + ); + } + + // Overhead on the Overhead IGP account + if !map_configuration_matches( + &overhead_igp_account.gas_overheads, + remote, + remote_domain, + config.overhead.as_ref(), + ) { + let overhead_config = GasOverheadConfig { + destination_domain: remote_domain, + gas_overhead: config.overhead, + }; + println!( + "Setting gas overhead for remote domain {:?} ({:?}) with config {:?}", + remote, remote_domain, overhead_config + ); + // For simplicity and to always be well within max tx sizes, just send one config at a time + let instruction = hyperlane_sealevel_igp::instruction::set_destination_gas_overheads( + program_id, + overhead_igp_account_pubkey, + ctx.payer_pubkey, + vec![overhead_config], + ) + .unwrap(); + + ctx.new_txn().add(instruction).send_with_payer(); + + println!( + "Set gas overhead for remote domain {:?} ({:?})", + remote, remote_domain + ); + } + } +} + +fn map_configuration_matches( + existing_map: &HashMap, + remote: &String, + remote_domain: u32, + new_config: Option<&T>, +) -> bool +where + T: PartialEq + std::fmt::Debug, +{ + let existing_config = existing_map.get(&remote_domain); + if existing_config == new_config { + println!( + "Configuration for remote domain {:?} ({:?}) matches expected config: {:?}", + remote, remote_domain, new_config + ); + true + } else { + println!( + "Configuration for remote domain {:?} ({:?}) does not match expected config. Current value: {:?}, expected value: {:?}", + remote, remote_domain, existing_config, new_config + ); + false + } } diff --git a/rust/sealevel/client/src/main.rs b/rust/sealevel/client/src/main.rs index c9ea55b548..eda64e9cb1 100644 --- a/rust/sealevel/client/src/main.rs +++ b/rust/sealevel/client/src/main.rs @@ -404,6 +404,7 @@ enum IgpSubCmd { DestinationGasOverhead(DestinationGasOverheadArgs), TransferIgpOwnership(TransferIgpOwnership), TransferOverheadIgpOwnership(TransferIgpOwnership), + Configure(ConfigureIgpArgs), } #[derive(Args)] @@ -425,12 +426,8 @@ struct InitIgpAccountArgs { #[arg(long)] chain: String, #[arg(long)] - chain_config_file: PathBuf, - #[arg(long)] context: Option, #[arg(long)] - gas_oracle_config_file: Option, - #[arg(long)] account_salt: Option, // optional salt for deterministic account creation } @@ -443,14 +440,10 @@ struct InitOverheadIgpAccountArgs { #[arg(long)] chain: String, #[arg(long)] - chain_config_file: PathBuf, - #[arg(long)] inner_igp_account: Pubkey, #[arg(long)] context: Option, #[arg(long)] - overhead_config_file: Option, - #[arg(long)] account_salt: Option, // optional salt for deterministic account creation } @@ -555,6 +548,20 @@ struct SetGasOverheadArgs { gas_overhead: u64, } +#[derive(Args)] +struct ConfigureIgpArgs { + #[arg(long)] + program_id: Pubkey, + #[arg(long)] + chain: String, + #[arg(long)] + gas_oracle_config_file: PathBuf, + #[arg(long)] + chain_config_file: PathBuf, + #[arg(long)] + account_salt: Option, +} + #[derive(Args)] struct ValidatorAnnounceCmd { #[command(subcommand)] diff --git a/rust/sealevel/environments/local-e2e/gas-oracle-configs.json b/rust/sealevel/environments/local-e2e/gas-oracle-configs.json index 1d69e4c3bf..896508f142 100644 --- a/rust/sealevel/environments/local-e2e/gas-oracle-configs.json +++ b/rust/sealevel/environments/local-e2e/gas-oracle-configs.json @@ -1,20 +1,22 @@ -[ - { - "domain": 13375, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "10000000000000000000", - "gasPrice": "0", - "tokenDecimals": 18 +{ + "sealeveltest1": { + "sealeveltest2": { + "oracleConfig": { + "tokenExchangeRate": "1000000000000000000", + "gasPrice": "1", + "tokenDecimals": 9 + }, + "overhead": 100000 } }, - { - "domain": 13376, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "10000000000000000000", - "gasPrice": "0", - "tokenDecimals": 18 + "sealeveltest2": { + "sealeveltest1": { + "oracleConfig": { + "tokenExchangeRate": "1000000000000000000", + "gasPrice": "1", + "tokenDecimals": 9 + }, + "overhead": 100000 } } -] +} diff --git a/rust/sealevel/environments/local-e2e/overheads.json b/rust/sealevel/environments/local-e2e/overheads.json deleted file mode 100644 index 653fbf55f2..0000000000 --- a/rust/sealevel/environments/local-e2e/overheads.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "destinationDomain": 13375, - "gasOverhead": 10000 - }, - { - "destinationDomain": 13376, - "gasOverhead": 10000 - } -] diff --git a/rust/sealevel/environments/mainnet3/chain-config.json b/rust/sealevel/environments/mainnet3/chain-config.json index 0688d72632..ba45f6ade4 100644 --- a/rust/sealevel/environments/mainnet3/chain-config.json +++ b/rust/sealevel/environments/mainnet3/chain-config.json @@ -243,6 +243,45 @@ ], "technicalStack": "arbitrumnitro" }, + "artela": { + "blockExplorers": [ + { + "apiUrl": "https://artscan.artela.network/api", + "family": "blockscout", + "name": "Artela Explorer", + "url": "https://artscan.artela.network" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 2, + "reorgPeriod": 5 + }, + "chainId": 11820, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Artela", + "domainId": 11820, + "gasCurrencyCoinGeckoId": "artela", + "name": "artela", + "nativeToken": { + "decimals": 18, + "name": "Artela", + "symbol": "ART" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://node-euro.artela.network/rpc" + }, + { + "http": "https://node-hongkong.artela.network/rpc" + } + ], + "technicalStack": "other" + }, "arthera": { "blockExplorers": [ { @@ -1621,6 +1660,7 @@ "displayName": "Form", "domainId": 478, "gasCurrencyCoinGeckoId": "ethereum", + "gnosisSafeTransactionServiceUrl": "https://prod.form.keypersafe.xyz/", "name": "form", "nativeToken": { "decimals": 18, @@ -1802,6 +1842,42 @@ ], "technicalStack": "arbitrumnitro" }, + "guru": { + "blockExplorers": [ + { + "apiUrl": "https://blockscout.gurunetwork.ai/api", + "family": "blockscout", + "name": "Guru Explorer", + "url": "https://blockscout.gurunetwork.ai" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 1, + "reorgPeriod": 5 + }, + "chainId": 260, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Guru Network", + "domainId": 260, + "gasCurrencyCoinGeckoId": "guru-network", + "name": "guru", + "nativeToken": { + "decimals": 18, + "name": "Guru Network", + "symbol": "GURU" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.gurunetwork.ai/archive/260" + } + ], + "technicalStack": "opstack" + }, "harmony": { "blockExplorers": [ { @@ -1847,6 +1923,42 @@ ], "technicalStack": "other" }, + "hemi": { + "blockExplorers": [ + { + "apiUrl": "https://explorer.hemi.xyz/api", + "family": "blockscout", + "name": "Hemi Explorer", + "url": "https://explorer.hemi.xyz" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 12, + "reorgPeriod": 5 + }, + "chainId": 43111, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Hemi Network", + "domainId": 43111, + "gasCurrencyCoinGeckoId": "ethereum", + "name": "hemi", + "nativeToken": { + "decimals": 18, + "name": "Ether", + "symbol": "ETH" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.hemi.network/rpc" + } + ], + "technicalStack": "other" + }, "immutablezkevmmainnet": { "blockExplorers": [ { @@ -1955,6 +2067,7 @@ "displayName": "Ink", "domainId": 57073, "gasCurrencyCoinGeckoId": "ethereum", + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-ink.safe.global/", "name": "ink", "nativeToken": { "decimals": 18, @@ -2021,6 +2134,9 @@ } ], "rpcUrls": [ + { + "http": "https://injective-rpc.publicnode.com:443" + }, { "http": "https://sentry.tm.injective.network:443" } @@ -2692,6 +2808,47 @@ ], "technicalStack": "other" }, + "nero": { + "blockExplorers": [ + { + "apiUrl": "https://api.neroscan.io/api", + "family": "etherscan", + "name": "Neroscan", + "url": "https://www.neroscan.io" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 3, + "reorgPeriod": 5 + }, + "chainId": 1689, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Nero", + "domainId": 1689, + "gasCurrencyCoinGeckoId": "nerochain", + "gnosisSafeTransactionServiceUrl": "https://multisign.nerochain.io/txs/", + "name": "nero", + "nativeToken": { + "decimals": 18, + "name": "Nero", + "symbol": "NERO" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.nerochain.io" + } + ], + "technicalStack": "other", + "transactionOverrides": { + "maxFeePerGas": 10000000000, + "maxPriorityFeePerGas": 1000000000 + } + }, "neutron": { "bech32Prefix": "neutron", "blockExplorers": [ @@ -3574,10 +3731,10 @@ "soneium": { "blockExplorers": [ { - "apiUrl": "https://explorer.soneium.org/api", + "apiUrl": "https://soneium.blockscout.com/api", "family": "blockscout", "name": "Soneium Explorer", - "url": "https://explorer.soneium.org" + "url": "https://soneium.blockscout.com" } ], "blocks": { @@ -3965,6 +4122,45 @@ ], "technicalStack": "other" }, + "torus": { + "blockExplorers": [ + { + "apiUrl": "https://api.blockscout.torus.network/api", + "family": "blockscout", + "name": "Torus Explorer", + "url": "https://blockscout.torus.network" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 8, + "reorgPeriod": "finalized" + }, + "chainId": 21000, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Torus", + "domainId": 21000, + "gasCurrencyCoinGeckoId": "torus", + "name": "torus", + "nativeToken": { + "decimals": 18, + "name": "Torus", + "symbol": "TORUS" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://api-hyperlane.nodes.torus.network" + }, + { + "http": "https://api.torus.network" + } + ], + "technicalStack": "polkadotsubstrate" + }, "treasure": { "blockExplorers": [ { @@ -4001,6 +4197,45 @@ ], "technicalStack": "zksync" }, + "trumpchain": { + "blockExplorers": [ + { + "apiUrl": "https://explorer.trumpchain.dev/api", + "family": "blockscout", + "name": "TRUMPCHAIN Explorer", + "url": "https://explorer.trumpchain.dev" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 1, + "reorgPeriod": 0 + }, + "chainId": 4547, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "TRUMPCHAIN", + "domainId": 4547, + "gasCurrencyCoinGeckoId": "official-trump", + "index": { + "from": 18 + }, + "name": "trumpchain", + "nativeToken": { + "decimals": 18, + "name": "TRUMP", + "symbol": "TRUMP" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.trumpchain.dev" + } + ], + "technicalStack": "arbitrumnitro" + }, "unichain": { "blockExplorers": [ { @@ -4228,6 +4463,42 @@ ], "technicalStack": "polygoncdk" }, + "xpla": { + "blockExplorers": [ + { + "apiUrl": "https://explorer.xpla.io/mainnet/api", + "family": "other", + "name": "XPLA Explorer", + "url": "https://explorer.xpla.io/mainnet" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 6, + "reorgPeriod": 5 + }, + "chainId": 37, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "XPLA", + "domainId": 37, + "gasCurrencyCoinGeckoId": "xpla", + "name": "xpla", + "nativeToken": { + "decimals": 18, + "name": "XPLA", + "symbol": "XPLA" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://dimension-evm-rpc.xpla.dev" + } + ], + "technicalStack": "other" + }, "zeronetwork": { "blockExplorers": [ { diff --git a/rust/sealevel/environments/mainnet3/eclipsemainnet/gas-oracle-configs-eclipsemainnet.json b/rust/sealevel/environments/mainnet3/eclipsemainnet/gas-oracle-configs-eclipsemainnet.json deleted file mode 100644 index 22fb0da678..0000000000 --- a/rust/sealevel/environments/mainnet3/eclipsemainnet/gas-oracle-configs-eclipsemainnet.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "domain": 1, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "15000000000000000000", - "gasPrice": "10000000000", - "tokenDecimals": 18 - } - }, - { - "domain": 1399811149, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "887000000000000000", - "gasPrice": "20", - "tokenDecimals": 9 - } - }, - { - "domain": 745, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "435246388284187", - "gasPrice": "7", - "tokenDecimals": 6 - } - } -] diff --git a/rust/sealevel/environments/mainnet3/gas-oracle-configs.json b/rust/sealevel/environments/mainnet3/gas-oracle-configs.json new file mode 100644 index 0000000000..26f2b592ef --- /dev/null +++ b/rust/sealevel/environments/mainnet3/gas-oracle-configs.json @@ -0,0 +1,136 @@ +{ + "solanamainnet": { + "trumpchain": { + "oracleConfig": { + "tokenExchangeRate": "4545000000000000000", + "gasPrice": "11271716004", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "optimism": { + "oracleConfig": { + "tokenExchangeRate": "263105000000000000000", + "gasPrice": "486782274", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "artela": { + "oracleConfig": { + "tokenExchangeRate": "83333333333333333", + "gasPrice": "614759390835", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "base": { + "oracleConfig": { + "tokenExchangeRate": "263105000000000000000", + "gasPrice": "486782274", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "arbitrum": { + "oracleConfig": { + "tokenExchangeRate": "263105000000000000000", + "gasPrice": "486782274", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "avalanche": { + "oracleConfig": { + "tokenExchangeRate": "2866666666666666666", + "gasPrice": "25000000001", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "flowmainnet": { + "oracleConfig": { + "tokenExchangeRate": "57039416666666666", + "gasPrice": "898149950159", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "form": { + "oracleConfig": { + "tokenExchangeRate": "263105000000000000000", + "gasPrice": "194712945", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "worldchain": { + "oracleConfig": { + "tokenExchangeRate": "263105000000000000000", + "gasPrice": "194712945", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "ethereum": { + "oracleConfig": { + "tokenExchangeRate": "263105000000000000000", + "gasPrice": "20047740244", + "tokenDecimals": 18 + }, + "overhead": 166887 + }, + "eclipsemainnet": { + "oracleConfig": { + "tokenExchangeRate": "26310500000000000", + "gasPrice": "1625", + "tokenDecimals": 9 + }, + "overhead": 600000 + }, + "soon": { + "oracleConfig": { + "tokenExchangeRate": "26310500000000000", + "gasPrice": "1625", + "tokenDecimals": 9 + }, + "overhead": 600000 + } + }, + "eclipsemainnet": { + "ethereum": { + "oracleConfig": { + "tokenExchangeRate": "15000000000000000000", + "gasPrice": "20047740244", + "tokenDecimals": 18 + }, + "overhead": 166460 + }, + "solanamainnet": { + "oracleConfig": { + "tokenExchangeRate": "85517188954979", + "gasPrice": "170940", + "tokenDecimals": 9 + }, + "overhead": 600000 + }, + "stride": { + "oracleConfig": { + "tokenExchangeRate": "235829801790", + "gasPrice": "4133", + "tokenDecimals": 6 + }, + "overhead": 600000 + } + }, + "soon": { + "solanamainnet": { + "oracleConfig": { + "tokenExchangeRate": "85517188954979", + "gasPrice": "170940", + "tokenDecimals": 9 + }, + "overhead": 600000 + } + } +} diff --git a/rust/sealevel/environments/mainnet3/solanamainnet/gas-oracle-configs-solanamainnet.json b/rust/sealevel/environments/mainnet3/solanamainnet/gas-oracle-configs-solanamainnet.json deleted file mode 100644 index 463990b543..0000000000 --- a/rust/sealevel/environments/mainnet3/solanamainnet/gas-oracle-configs-solanamainnet.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "domain": 1408864445, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "25036363636360000000", - "gasPrice": "2", - "tokenDecimals": 9 - } - }, - { - "domain": 50075007, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "25036363636360000000", - "gasPrice": "2", - "tokenDecimals": 9 - } - } -] diff --git a/rust/sealevel/environments/mainnet3/soon/gas-oracle-configs-soon.json b/rust/sealevel/environments/mainnet3/soon/gas-oracle-configs-soon.json deleted file mode 100644 index 4e0dad08ce..0000000000 --- a/rust/sealevel/environments/mainnet3/soon/gas-oracle-configs-soon.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "domain": 1, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "15000000000000000000", - "gasPrice": "10000000000", - "tokenDecimals": 18 - } - }, - { - "domain": 1399811149, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "887000000000000000", - "gasPrice": "20", - "tokenDecimals": 9 - } - } -] diff --git a/rust/sealevel/environments/testnet4/gas-oracle-configs.json b/rust/sealevel/environments/testnet4/gas-oracle-configs.json index cadbe1ad78..c56c311b79 100644 --- a/rust/sealevel/environments/testnet4/gas-oracle-configs.json +++ b/rust/sealevel/environments/testnet4/gas-oracle-configs.json @@ -1,38 +1,22 @@ -[ - { - "domain": 11155111, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "10000000000000000000", - "gasPrice": "15000000000", - "tokenDecimals": 18 - } - }, - { - "domain": 1399811150, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "10000000000000000000", - "gasPrice": "28", - "tokenDecimals": 9 - } - }, - { - "domain": 239092742, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "10000000000000000000", - "gasPrice": "28", - "tokenDecimals": 9 - } - }, - { - "domain": 15153042, - "gasOracle": { - "type": "remoteGasData", - "tokenExchangeRate": "10000000000000000000", - "gasPrice": "28", - "tokenDecimals": 9 - } +{ + "solanatestnet": { + "sonicsvmtestnet": { + "oracleConfig": { + "tokenExchangeRate": "1500000000000000", + "gasPrice": "1000", + "tokenDecimals": 9 + }, + "overhead": 600000 } -] \ No newline at end of file + }, + "sonicsvmtestnet": { + "solanatestnet": { + "oracleConfig": { + "tokenExchangeRate": "1500000000000000", + "gasPrice": "1000", + "tokenDecimals": 9 + }, + "overhead": 600000 + } + } +} diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index 1ca07a9a70..bad95f5c3b 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -221,6 +221,7 @@ export const createIGPConfig = callWithConfigCreationLogs( // Calculate storage gas oracle config const oracleConfig = getLocalStorageGasOracleConfig({ local: localChain, + localProtocolType: context.multiProvider.getProtocol(localChain), gasOracleParams: prices, exchangeRateMarginPct, }); diff --git a/typescript/infra/config/environments/mainnet3/igp.ts b/typescript/infra/config/environments/mainnet3/igp.ts index 5c2dfd6efc..c29f35d7b9 100644 --- a/typescript/infra/config/environments/mainnet3/igp.ts +++ b/typescript/infra/config/environments/mainnet3/igp.ts @@ -4,22 +4,19 @@ import { ChainTechnicalStack, HookType, IgpConfig, - getTokenExchangeRateFromValues, } from '@hyperlane-xyz/sdk'; import { exclude, objMap } from '@hyperlane-xyz/utils'; import { AllStorageGasOracleConfigs, - EXCHANGE_RATE_MARGIN_PCT, getAllStorageGasOracleConfigs, getOverhead, } from '../../../src/config/gas-oracle.js'; -import { mustGetChainNativeToken } from '../../../src/utils/utils.js'; import { getChain } from '../../registry.js'; import { ethereumChainNames } from './chains.js'; import gasPrices from './gasPrices.json'; -import { DEPLOYER, ethereumChainOwners } from './owners.js'; +import { DEPLOYER, chainOwners } from './owners.js'; import { supportedChainNames } from './supportedChainNames.js'; import rawTokenPrices from './tokenPrices.json'; @@ -42,24 +39,14 @@ export function getOverheadWithOverrides(local: ChainName, remote: ChainName) { const storageGasOracleConfig: AllStorageGasOracleConfigs = getAllStorageGasOracleConfigs( supportedChainNames, + tokenPrices, gasPrices, - (local, remote) => - getTokenExchangeRateFromValues({ - local, - remote, - tokenPrices, - exchangeRateMarginPct: EXCHANGE_RATE_MARGIN_PCT, - decimals: { - local: mustGetChainNativeToken(local).decimals, - remote: mustGetChainNativeToken(remote).decimals, - }, - }), - (local) => parseFloat(tokenPrices[local]), (local, remote) => getOverheadWithOverrides(local, remote), + true, ); export const igp: ChainMap = objMap( - ethereumChainOwners, + chainOwners, (local, owner): IgpConfig => ({ type: HookType.INTERCHAIN_GAS_PAYMASTER, ...owner, diff --git a/typescript/infra/config/environments/test/gas-oracle.ts b/typescript/infra/config/environments/test/gas-oracle.ts index 65c09d9cdb..e2ee97f0a5 100644 --- a/typescript/infra/config/environments/test/gas-oracle.ts +++ b/typescript/infra/config/environments/test/gas-oracle.ts @@ -1,11 +1,4 @@ -import { BigNumber, ethers } from 'ethers'; - -import { - ChainMap, - ChainName, - GasPriceConfig, - TOKEN_EXCHANGE_RATE_DECIMALS, -} from '@hyperlane-xyz/sdk'; +import { ChainMap, GasPriceConfig } from '@hyperlane-xyz/sdk'; import { AllStorageGasOracleConfigs, @@ -14,31 +7,29 @@ import { import { testChainNames } from './chains.js'; -const TEST_TOKEN_EXCHANGE_RATE = ethers.utils.parseUnits( - '1', - TOKEN_EXCHANGE_RATE_DECIMALS, -); +const TEST_TOKEN_EXCHANGE_RATE = '1'; const TEST_GAS_PRICE_CONFIG: GasPriceConfig = { amount: '2', decimals: 9, // gwei }; +const tokenPrices: ChainMap = { + test1: TEST_TOKEN_EXCHANGE_RATE, + test2: TEST_TOKEN_EXCHANGE_RATE, + test3: TEST_TOKEN_EXCHANGE_RATE, +}; + const gasPrices: ChainMap = { test1: TEST_GAS_PRICE_CONFIG, test2: TEST_GAS_PRICE_CONFIG, test3: TEST_GAS_PRICE_CONFIG, }; -function getTokenExchangeRate( - _local: ChainName, - _remote: ChainName, -): BigNumber { - return TEST_TOKEN_EXCHANGE_RATE; -} - export const storageGasOracleConfig: AllStorageGasOracleConfigs = getAllStorageGasOracleConfigs( testChainNames, + tokenPrices, gasPrices, - getTokenExchangeRate, + (_local, _remote) => 0, + false, ); diff --git a/typescript/infra/config/environments/testnet4/gasPrices.json b/typescript/infra/config/environments/testnet4/gasPrices.json index 2224fa5795..f2bbf1c3a9 100644 --- a/typescript/infra/config/environments/testnet4/gasPrices.json +++ b/typescript/infra/config/environments/testnet4/gasPrices.json @@ -96,7 +96,7 @@ "decimals": 9 }, "solanatestnet": { - "amount": "0.0001", + "amount": "0.01", "decimals": 1 }, "soneiumtestnet": { @@ -108,7 +108,7 @@ "decimals": 9 }, "sonicsvmtestnet": { - "amount": "0.0001", + "amount": "0.01", "decimals": 1 }, "suavetoliman": { diff --git a/typescript/infra/config/environments/testnet4/igp.ts b/typescript/infra/config/environments/testnet4/igp.ts index 0cabe5b69a..f06a3e4b87 100644 --- a/typescript/infra/config/environments/testnet4/igp.ts +++ b/typescript/infra/config/environments/testnet4/igp.ts @@ -1,18 +1,11 @@ -import { - ChainMap, - HookType, - IgpConfig, - getTokenExchangeRateFromValues, -} from '@hyperlane-xyz/sdk'; +import { ChainMap, HookType, IgpConfig } from '@hyperlane-xyz/sdk'; import { Address, exclude, objMap } from '@hyperlane-xyz/utils'; import { AllStorageGasOracleConfigs, - EXCHANGE_RATE_MARGIN_PCT, getAllStorageGasOracleConfigs, getOverhead, } from '../../../src/config/gas-oracle.js'; -import { mustGetChainNativeToken } from '../../../src/utils/utils.js'; import { ethereumChainNames } from './chains.js'; import gasPrices from './gasPrices.json'; @@ -25,18 +18,10 @@ const tokenPrices: ChainMap = rawTokenPrices; export const storageGasOracleConfig: AllStorageGasOracleConfigs = getAllStorageGasOracleConfigs( supportedChainNames, + tokenPrices, gasPrices, - (local, remote) => - getTokenExchangeRateFromValues({ - local, - remote, - tokenPrices, - exchangeRateMarginPct: EXCHANGE_RATE_MARGIN_PCT, - decimals: { - local: mustGetChainNativeToken(local).decimals, - remote: mustGetChainNativeToken(remote).decimals, - }, - }), + (local, remote) => getOverhead(local, remote, ethereumChainNames), + false, ); export const igp: ChainMap = objMap( diff --git a/typescript/infra/config/environments/testnet4/tokenPrices.json b/typescript/infra/config/environments/testnet4/tokenPrices.json index 0bea8df9b3..3dec117300 100644 --- a/typescript/infra/config/environments/testnet4/tokenPrices.json +++ b/typescript/infra/config/environments/testnet4/tokenPrices.json @@ -25,6 +25,7 @@ "sepolia": "10", "solanatestnet": "10", "soneiumtestnet": "10", + "sonictestnet": "10", "sonicblaze": "10", "sonicsvmtestnet": "10", "suavetoliman": "10", diff --git a/typescript/infra/config/registry.ts b/typescript/infra/config/registry.ts index 8b9ea5edb7..526c23c14b 100644 --- a/typescript/infra/config/registry.ts +++ b/typescript/infra/config/registry.ts @@ -104,7 +104,9 @@ export function getWarpCoreConfig(warpRouteId: string): WarpCoreConfig { return warpRouteConfig; } -export function getWarpAddresses(warpRouteId: string) { +export function getWarpAddresses( + warpRouteId: string, +): ChainMap { const warpCoreConfig = getWarpCoreConfig(warpRouteId); return warpConfigToWarpAddresses(warpCoreConfig); } diff --git a/typescript/infra/scripts/agent-utils.ts b/typescript/infra/scripts/agent-utils.ts index 0d65657a9f..2b0a788b2b 100644 --- a/typescript/infra/scripts/agent-utils.ts +++ b/typescript/infra/scripts/agent-utils.ts @@ -177,6 +177,13 @@ export function withChainsRequired( return withChains(args, chainOptions).demandOption('chains'); } +export function withOutputFile(args: Argv) { + return args + .describe('outFile', 'output file') + .string('outFile') + .alias('o', 'outFile'); +} + export function withWarpRouteId(args: Argv) { return args.describe('warpRouteId', 'warp route id').string('warpRouteId'); } diff --git a/typescript/infra/scripts/sealevel-helpers/print-gas-oracles.ts b/typescript/infra/scripts/sealevel-helpers/print-gas-oracles.ts new file mode 100644 index 0000000000..8e61cd2ce6 --- /dev/null +++ b/typescript/infra/scripts/sealevel-helpers/print-gas-oracles.ts @@ -0,0 +1,140 @@ +import { + ChainMap, + ChainName, + ProtocolAgnositicGasOracleConfig, +} from '@hyperlane-xyz/sdk'; +import { + ProtocolType, + objFilter, + objMap, + stringifyObject, +} from '@hyperlane-xyz/utils'; + +import { WarpRouteIds } from '../../config/environments/mainnet3/warp/warpIds.js'; +import { getChain, getWarpAddresses } from '../../config/registry.js'; +import { DeployEnvironment } from '../../src/config/environment.js'; +import { writeJsonAtPath } from '../../src/utils/utils.js'; +import { getArgs, withOutputFile } from '../agent-utils.js'; +import { getEnvironmentConfig } from '../core-utils.js'; + +// This script exists to print the gas oracle configs for a given environment +// so they can easily be copied into the Sealevel tooling. :'( + +interface GasOracleConfigWithOverhead { + oracleConfig: ProtocolAgnositicGasOracleConfig; + overhead?: number; +} + +async function main() { + const { environment, outFile } = await withOutputFile(getArgs()).argv; + + const environmentConfig = getEnvironmentConfig(environment); + + const allConnectedChains = getChainConnections(environment); + + // Construct a nested map of origin -> destination -> { oracleConfig, overhead } + let gasOracles = objMap(environmentConfig.igp, (origin, igpConfig) => { + // Only SVM origins for now + if (getChain(origin).protocol !== ProtocolType.Sealevel) { + return undefined; + } + + // If there's no oracle config, don't do anything for this origin + if (!igpConfig.oracleConfig) { + return undefined; + } + // Get the set of chains that are connected to this origin via warp routes + const connectedChainsSet = allConnectedChains[origin]; + if (!connectedChainsSet) { + return undefined; + } + const connectedChains = [...connectedChainsSet]; + + return connectedChains.reduce((agg, destination) => { + const oracleConfig = igpConfig.oracleConfig[destination]; + if (oracleConfig.tokenDecimals === undefined) { + throw new Error( + `Token decimals not defined for ${origin} -> ${destination}`, + ); + } + agg[destination] = { + oracleConfig, + overhead: igpConfig?.overhead?.[destination], + }; + return agg; + }, {} as ChainMap); + }); + + // Filter out undefined values + gasOracles = objFilter( + gasOracles, + (_, value): value is ChainMap | undefined => + value !== undefined, + ); + + console.log(stringifyObject(gasOracles, 'json', 2)); + + if (outFile) { + console.log(`Writing config to ${outFile}`); + writeJsonAtPath(outFile, gasOracles); + } +} + +// Gets the chains in the provided warp route +function getWarpChains(warpRouteId: string): ChainName[] { + const warpRouteAddresses = getWarpAddresses(warpRouteId); + return Object.keys(warpRouteAddresses); +} + +// Because there is a limit to how many chains we want to figure in an SVM IGP, +// we limit the chains to only those that are connected via warp routes. +// Returns a record of origin chain -> set of chains that are connected via warp routes. +function getChainConnections( + environment: DeployEnvironment, +): ChainMap> { + // A list of connected chains + let connectedChains = []; + + if (environment === 'mainnet3') { + // All the mainnet3 warp route chains + connectedChains = [ + // Some branch juggling for the new TRUMP route temporarily + // requires these chains to be hardcoded: + // TRUMP/solanamainnet-trumpchain + ['solanamainnet', 'trumpchain'], + // For the massive TRUMP warp route, which is undergoing an extension atm + ['solanamainnet', 'optimism'], + // All warp routes + ...Object.values(WarpRouteIds).map(getWarpChains), + ]; + } else if (environment === 'testnet4') { + connectedChains = [ + // As testnet warp routes are not tracked well, hardcode the connected chains. + // For SOL/solanatestnet-sonicsvmtestnet + ['solanatestnet', 'sonicsvmtestnet'], + ]; + } else { + throw new Error(`Unknown environment: ${environment}`); + } + + return connectedChains.reduce((agg, chains) => { + // Make sure each chain is connected to every other chain + chains.forEach((chainA) => { + chains.forEach((chainB) => { + if (chainA === chainB) { + return; + } + if (agg[chainA] === undefined) { + agg[chainA] = new Set(); + } + agg[chainA].add(chainB as ChainName); + }); + }); + return agg; + }, {} as ChainMap>); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/infra/scripts/sealevel-helpers/print-multisig-ism-config.ts b/typescript/infra/scripts/sealevel-helpers/print-multisig-ism-config.ts index 69646ff013..bc3e634c77 100644 --- a/typescript/infra/scripts/sealevel-helpers/print-multisig-ism-config.ts +++ b/typescript/infra/scripts/sealevel-helpers/print-multisig-ism-config.ts @@ -39,8 +39,6 @@ async function main() { } } - console.warn; - console.log(JSON.stringify(config, null, 2)); } diff --git a/typescript/infra/scripts/warp-routes/generate-warp-config.ts b/typescript/infra/scripts/warp-routes/generate-warp-config.ts index 077cef0d4c..47a39c16d8 100644 --- a/typescript/infra/scripts/warp-routes/generate-warp-config.ts +++ b/typescript/infra/scripts/warp-routes/generate-warp-config.ts @@ -4,15 +4,17 @@ import { WarpRouteDeployConfigSchema } from '@hyperlane-xyz/sdk'; import { getWarpConfig } from '../../config/warp.js'; import { writeYamlAtPath } from '../../src/utils/utils.js'; -import { getArgs, withWarpRouteIdRequired } from '../agent-utils.js'; +import { + getArgs, + withOutputFile, + withWarpRouteIdRequired, +} from '../agent-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js'; async function main() { - const { warpRouteId, environment, outFile } = await withWarpRouteIdRequired( - getArgs(), - ) - .string('outFile') - .describe('outFile', 'The file to write the config to').argv; + const { warpRouteId, environment, outFile } = await withOutputFile( + withWarpRouteIdRequired(getArgs()), + ).argv; const { multiProvider } = await getHyperlaneCore(environment); const envConfig = getEnvironmentConfig(environment); diff --git a/typescript/infra/src/config/gas-oracle.ts b/typescript/infra/src/config/gas-oracle.ts index 622ad18b36..76d368f100 100644 --- a/typescript/infra/src/config/gas-oracle.ts +++ b/typescript/infra/src/config/gas-oracle.ts @@ -1,17 +1,33 @@ +import { BigNumber as BigNumberJs } from 'bignumber.js'; import chalk from 'chalk'; import { BigNumber, ethers } from 'ethers'; import { + ChainGasOracleParams, ChainMap, ChainName, GasPriceConfig, + ProtocolAgnositicGasOracleConfig, StorageGasOracleConfig, - TOKEN_EXCHANGE_RATE_SCALE, defaultMultisigConfigs, + getLocalStorageGasOracleConfig, + getProtocolExchangeRateScale, multisigIsmVerificationCost, } from '@hyperlane-xyz/sdk'; +import { + ProtocolType, + assert, + convertDecimals, + convertDecimalsToIntegerString, + fromWei, + toWei, +} from '@hyperlane-xyz/utils'; -import { isEthereumProtocolChain } from '../utils/utils.js'; +import { getChain } from '../../config/registry.js'; +import { + isEthereumProtocolChain, + mustGetChainNativeToken, +} from '../utils/utils.js'; // gas oracle configs for each chain, which includes // a map for each chain's remote chains @@ -32,92 +48,104 @@ export const TYPICAL_HANDLE_GAS_USAGE = 50_000; function getLocalStorageGasOracleConfigOverride( local: ChainName, remotes: ChainName[], + tokenPrices: ChainMap, gasPrices: ChainMap, - getTokenExchangeRate: (local: ChainName, remote: ChainName) => BigNumber, - getTokenUsdPrice?: (chain: ChainName) => number, - getOverhead?: (local: ChainName, remote: ChainName) => number, + getOverhead: (local: ChainName, remote: ChainName) => number, + applyMinUsdCost: boolean, ): ChainMap { - return remotes.reduce((agg, remote) => { - let exchangeRate = getTokenExchangeRate(local, remote); - if (!gasPrices[remote]) { - // Will run into this case when adding new chains - console.warn(chalk.yellow(`No gas price set for ${remote}`)); - return agg; - } + const localProtocolType = getChain(local).protocol; + const localExchangeRateScale = + getProtocolExchangeRateScale(localProtocolType); + const localNativeTokenDecimals = mustGetChainNativeToken(local).decimals; - // First parse as a number, so we have floating point precision. - // Recall it's possible to have gas prices that are not integers, even - // after converting to the "wei" version of the token. - let gasPrice = - parseFloat(gasPrices[remote].amount) * - Math.pow(10, gasPrices[remote].decimals); - if (isNaN(gasPrice)) { - throw new Error( - `Invalid gas price for chain ${remote}: ${gasPrices[remote]}`, - ); + // Construct the gas oracle params for each remote chain + const gasOracleParams = [local, ...remotes].reduce((agg, remote) => { + agg[remote] = { + gasPrice: gasPrices[remote], + nativeToken: { + price: tokenPrices[remote], + decimals: mustGetChainNativeToken(remote).decimals, + }, + }; + return agg; + }, {} as ChainMap); + + // Modifier to adjust the gas price to meet minimum USD cost requirements. + const gasPriceModifier = ( + local: ChainName, + remote: ChainName, + gasOracleConfig: ProtocolAgnositicGasOracleConfig, + ): BigNumberJs.Value => { + if (!applyMinUsdCost) { + return gasOracleConfig.gasPrice; } - // We have very little precision and ultimately need an integer value for - // the gas price that will be set on-chain. We scale up the gas price and - // scale down the exchange rate by the same factor. - if (gasPrice < 10 && gasPrice % 1 !== 0) { - // Scale up the gas price by 1e4 - const gasPriceScalingFactor = 1e4; - - // Check that there's no significant underflow when applying - // this to the exchange rate: - const adjustedExchangeRate = exchangeRate.div(gasPriceScalingFactor); - const recoveredExchangeRate = adjustedExchangeRate.mul( - gasPriceScalingFactor, - ); - if (recoveredExchangeRate.mul(100).div(exchangeRate).lt(99)) { - throw new Error('Too much underflow when downscaling exchange rate'); - } + const typicalRemoteGasAmount = getTypicalRemoteGasAmount( + local, + remote, + getOverhead, + ); + const localTokenUsdPrice = parseFloat(tokenPrices[local]); + const typicalIgpQuoteUsd = getUsdQuote( + localTokenUsdPrice, + localExchangeRateScale, + localNativeTokenDecimals, + localProtocolType, + gasOracleConfig, + typicalRemoteGasAmount, + ); - // Apply the scaling factor - exchangeRate = adjustedExchangeRate; - gasPrice *= gasPriceScalingFactor; + const minUsdCost = getMinUsdCost(local, remote); + + // If the quote is already above the minimum cost, don't adjust the gas price! + if (typicalIgpQuoteUsd >= minUsdCost) { + return gasOracleConfig.gasPrice; } - // Our integer gas price. - let gasPriceBn = BigNumber.from(Math.ceil(gasPrice)); + // If we've gotten here, the quote is less than the minimum cost and we + // need to adjust the gas price. - // If we have access to these, let's use the USD prices to apply some minimum - // typical USD payment heuristics. - if (getTokenUsdPrice && getOverhead) { - const typicalRemoteGasAmount = getTypicalRemoteGasAmount( - local, - remote, - getOverhead, - ); - const typicalIgpQuoteUsd = getUsdQuote( - local, - gasPriceBn, - exchangeRate, - typicalRemoteGasAmount, - getTokenUsdPrice, + // The minimum quote we want on the origin, in the lowest origin denomination. + const minIgpQuoteWei = toWei( + new BigNumberJs(minUsdCost).div(localTokenUsdPrice), + localNativeTokenDecimals, + ); + // The new gas price that will give us the minimum quote. + // We use a BigNumberJs to allow for non-integer gas prices. + // Later in the process, this is made integer-friendly. + // This calculation expects that the token exchange rate accounts + // for decimals. + let newGasPrice = new BigNumberJs(minIgpQuoteWei) + .times(localExchangeRateScale.toString()) + .div( + new BigNumberJs(gasOracleConfig.tokenExchangeRate).times( + typicalRemoteGasAmount, + ), ); - const minUsdCost = getMinUsdCost(local, remote); - if (typicalIgpQuoteUsd < minUsdCost) { - // Adjust the gasPrice to meet the minimum cost - const minIgpQuote = ethers.utils.parseEther( - (minUsdCost / getTokenUsdPrice(local)).toPrecision(8), - ); - gasPriceBn = minIgpQuote - .mul(TOKEN_EXCHANGE_RATE_SCALE) - .div(exchangeRate.mul(typicalRemoteGasAmount)); - } + if (localProtocolType === ProtocolType.Sealevel) { + assert( + gasOracleConfig.tokenDecimals, + 'Token decimals must be defined for use by local Sealevel chains', + ); + // On Sealevel, the exchange rate doesn't consider decimals. + // We therefore explicitly convert decimals to remote decimals. + newGasPrice = convertDecimals( + localNativeTokenDecimals, + gasOracleConfig.tokenDecimals, + newGasPrice.toString(), + ); } + return newGasPrice; + }; - return { - ...agg, - [remote]: { - tokenExchangeRate: exchangeRate, - gasPrice: gasPriceBn, - }, - }; - }, {}); + return getLocalStorageGasOracleConfig({ + local, + localProtocolType, + gasOracleParams, + exchangeRateMarginPct: EXCHANGE_RATE_MARGIN_PCT, + gasPriceModifier, + }); } export function getTypicalRemoteGasAmount( @@ -137,6 +165,11 @@ function getMinUsdCost(local: ChainName, remote: ChainName): number { minUsdCost = Math.max(minUsdCost, 1.5); } + // For all SVM chains, min cost is 0.50 USD to cover rent needs + if (getChain(remote).protocol === ProtocolType.Sealevel) { + minUsdCost = Math.max(minUsdCost, 0.5); + } + const remoteMinCostOverrides: ChainMap = { // For Ethereum L2s, we need to account for the L1 DA costs that // aren't accounted for directly in the gas price. @@ -157,6 +190,8 @@ function getMinUsdCost(local: ChainName, remote: ChainName): number { taiko: 0.5, // Nexus adjustment neutron: 0.5, + // For Solana, special min cost + solanamainnet: 3, }; const override = remoteMinCostOverrides[remote]; if (override !== undefined) { @@ -167,18 +202,32 @@ function getMinUsdCost(local: ChainName, remote: ChainName): number { } function getUsdQuote( - local: ChainName, - gasPrice: BigNumber, - exchangeRate: BigNumber, + localTokenUsdPrice: number, + localExchangeRateScale: BigNumber, + localNativeTokenDecimals: number, + localProtocolType: ProtocolType, + gasOracleConfig: ProtocolAgnositicGasOracleConfig, remoteGasAmount: number, - getTokenUsdPrice: (chain: ChainName) => number, ): number { - const quote = gasPrice - .mul(exchangeRate) + let quote = BigNumber.from(gasOracleConfig.gasPrice) + .mul(gasOracleConfig.tokenExchangeRate) .mul(remoteGasAmount) - .div(TOKEN_EXCHANGE_RATE_SCALE); + .div(localExchangeRateScale) + .toString(); + if (localProtocolType === ProtocolType.Sealevel) { + assert( + gasOracleConfig.tokenDecimals, + 'Token decimals must be defined for use by local Sealevel chains', + ); + // Convert decimals to local decimals + quote = convertDecimals( + gasOracleConfig.tokenDecimals, + localNativeTokenDecimals, + quote, + ).toString(); + } const quoteUsd = - getTokenUsdPrice(local) * parseFloat(ethers.utils.formatEther(quote)); + localTokenUsdPrice * parseFloat(fromWei(quote, localNativeTokenDecimals)); return quoteUsd; } @@ -203,23 +252,34 @@ export function getOverhead( // Gets the map of remote gas oracle configs for each local chain export function getAllStorageGasOracleConfigs( chainNames: ChainName[], + tokenPrices: ChainMap, gasPrices: ChainMap, - getTokenExchangeRate: (local: ChainName, remote: ChainName) => BigNumber, - getTokenUsdPrice?: (chain: ChainName) => number, - getOverhead?: (local: ChainName, remote: ChainName) => number, + getOverhead: (local: ChainName, remote: ChainName) => number, + applyMinUsdCost: boolean = true, ): AllStorageGasOracleConfigs { - return chainNames.filter(isEthereumProtocolChain).reduce((agg, local) => { - const remotes = chainNames.filter((chain) => local !== chain); - return { - ...agg, - [local]: getLocalStorageGasOracleConfigOverride( - local, - remotes, - gasPrices, - getTokenExchangeRate, - getTokenUsdPrice, - getOverhead, - ), - }; - }, {}) as AllStorageGasOracleConfigs; + return chainNames + .filter((chain) => { + // For now, only support Ethereum and Sealevel chains. + // Cosmos chains should be supported in the future, but at the moment + // are more subject to loss of precision issues in the exchange rate, + // where we'd need to scale the gas price accordingly. + const protocol = getChain(chain).protocol; + return ( + protocol === ProtocolType.Ethereum || protocol === ProtocolType.Sealevel + ); + }) + .reduce((agg, local) => { + const remotes = chainNames.filter((chain) => local !== chain); + return { + ...agg, + [local]: getLocalStorageGasOracleConfigOverride( + local, + remotes, + tokenPrices, + gasPrices, + getOverhead, + applyMinUsdCost, + ), + }; + }, {}) as AllStorageGasOracleConfigs; } diff --git a/typescript/sdk/src/consts/igp.ts b/typescript/sdk/src/consts/igp.ts index 52dac668d0..9dcc662399 100644 --- a/typescript/sdk/src/consts/igp.ts +++ b/typescript/sdk/src/consts/igp.ts @@ -1,8 +1,47 @@ -import { ethers } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; -export const TOKEN_EXCHANGE_RATE_DECIMALS = 10; +import { ProtocolType } from '@hyperlane-xyz/utils'; -export const TOKEN_EXCHANGE_RATE_SCALE = ethers.utils.parseUnits( +export const TOKEN_EXCHANGE_RATE_DECIMALS_ETHEREUM = 10; + +export const TOKEN_EXCHANGE_RATE_SCALE_ETHEREUM = ethers.utils.parseUnits( + '1', + TOKEN_EXCHANGE_RATE_DECIMALS_ETHEREUM, +); + +export const TOKEN_EXCHANGE_RATE_DECIMALS_SEALEVEL = 19; + +export const TOKEN_EXCHANGE_RATE_SCALE_SEALEVEL = ethers.utils.parseUnits( '1', - TOKEN_EXCHANGE_RATE_DECIMALS, + TOKEN_EXCHANGE_RATE_DECIMALS_SEALEVEL, ); + +export const TOKEN_EXCHANGE_RATE_DECIMALS_COSMOS = 10; + +export const TOKEN_EXCHANGE_RATE_SCALE_COSMOS = ethers.utils.parseUnits( + '1', + TOKEN_EXCHANGE_RATE_DECIMALS_COSMOS, +); + +// Gets the number of decimals for the exchange rate on a particular origin protocol. +// Different smart contract implementations require different levels of precision. +export function getProtocolExchangeRateDecimals( + protocolType: ProtocolType, +): number { + switch (protocolType) { + case ProtocolType.Ethereum: + return TOKEN_EXCHANGE_RATE_DECIMALS_ETHEREUM; + case ProtocolType.Sealevel: + return TOKEN_EXCHANGE_RATE_DECIMALS_SEALEVEL; + case ProtocolType.Cosmos: + return TOKEN_EXCHANGE_RATE_DECIMALS_COSMOS; + default: + throw new Error(`Unsupported protocol type: ${protocolType}`); + } +} + +export function getProtocolExchangeRateScale( + protocolType: ProtocolType, +): BigNumber { + return BigNumber.from(10).pow(getProtocolExchangeRateDecimals(protocolType)); +} diff --git a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts index 6283d87fa4..77ae185db8 100644 --- a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts +++ b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts @@ -7,7 +7,7 @@ import { } from '@hyperlane-xyz/core'; import { eqAddress, rootLogger } from '@hyperlane-xyz/utils'; -import { TOKEN_EXCHANGE_RATE_SCALE } from '../consts/igp.js'; +import { TOKEN_EXCHANGE_RATE_SCALE_ETHEREUM } from '../consts/igp.js'; import { HyperlaneContracts } from '../contracts/types.js'; import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; @@ -132,7 +132,11 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer< !actual.tokenExchangeRate.eq(desired.tokenExchangeRate) ) { this.logger.info( - `${chain} -> ${remote}: ${serializeDifference(actual, desiredData)}`, + `${chain} -> ${remote}: ${serializeDifference( + this.multiProvider.getProtocol(chain), + actual, + desiredData, + )}`, ); configsToSet.push({ remoteDomain, @@ -144,7 +148,7 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer< const exampleRemoteGasCost = desiredData.tokenExchangeRate .mul(desiredData.gasPrice) .mul(exampleRemoteGas) - .div(TOKEN_EXCHANGE_RATE_SCALE); + .div(TOKEN_EXCHANGE_RATE_SCALE_ETHEREUM); this.logger.info( `${chain} -> ${remote}: ${exampleRemoteGas} remote gas cost: ${ethers.utils.formatEther( exampleRemoteGasCost, diff --git a/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts b/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts index 8eb66a6edb..c5279a9f92 100644 --- a/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts +++ b/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts @@ -47,6 +47,7 @@ describe('HyperlaneIgpDeployer', () => { testConfig[local].oracleConfig![remote] = { tokenExchangeRate: utils.parseUnits('2', 'gwei').toString(), gasPrice: utils.parseUnits('3', 'gwei').toString(), + tokenDecimals: 18, }; const localContracts = await deployer.deployContracts( diff --git a/typescript/sdk/src/gas/oracle/types.ts b/typescript/sdk/src/gas/oracle/types.ts index 99669b7ea3..d33558f1a0 100644 --- a/typescript/sdk/src/gas/oracle/types.ts +++ b/typescript/sdk/src/gas/oracle/types.ts @@ -1,7 +1,9 @@ import { ethers } from 'ethers'; import { z } from 'zod'; -import { TOKEN_EXCHANGE_RATE_DECIMALS } from '../../consts/igp.js'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { getProtocolExchangeRateDecimals } from '../../consts/igp.js'; export const StorageGasOracleConfigSchema = z.object({ gasPrice: z.string(), @@ -13,12 +15,25 @@ export type StorageGasOracleConfig = z.output< typeof StorageGasOracleConfigSchema >; +export const ProtocolAgnositicGasOracleConfigSchema = + StorageGasOracleConfigSchema.extend({ + // The number of decimals of the remote native token. + // Optional because it's not required by all protocol types. + tokenDecimals: z.number().optional(), + }); + +// Gas data to configure on a single destination chain. +export type ProtocolAgnositicGasOracleConfig = z.output< + typeof ProtocolAgnositicGasOracleConfigSchema +>; + export type OracleData = { tokenExchangeRate: ethers.BigNumber; gasPrice: ethers.BigNumber; }; export const formatGasOracleConfig = ( + localChainProtocol: ProtocolType, config: OracleData, ): { tokenExchangeRate: string; @@ -26,7 +41,7 @@ export const formatGasOracleConfig = ( } => ({ tokenExchangeRate: ethers.utils.formatUnits( config.tokenExchangeRate, - TOKEN_EXCHANGE_RATE_DECIMALS, + getProtocolExchangeRateDecimals(localChainProtocol), ), gasPrice: ethers.utils.formatUnits(config.gasPrice, 'gwei'), }); @@ -56,6 +71,7 @@ export const oracleConfigToOracleData = ( }); export const serializeDifference = ( + localChainProtocol: ProtocolType, actual: OracleData, expected: OracleData, ): string => { @@ -73,6 +89,6 @@ export const serializeDifference = ( expected.tokenExchangeRate.mul(expected.gasPrice), ); - const formatted = formatGasOracleConfig(expected); + const formatted = formatGasOracleConfig(localChainProtocol, expected); return `Exchange rate: ${formatted.tokenExchangeRate} (${tokenExchangeRateDiff}), Gas price: ${formatted.gasPrice} gwei (${gasPriceDiff}), Product diff: ${productDiff}`; }; diff --git a/typescript/sdk/src/gas/utils.ts b/typescript/sdk/src/gas/utils.ts index 6f007398c5..22397095bc 100644 --- a/typescript/sdk/src/gas/utils.ts +++ b/typescript/sdk/src/gas/utils.ts @@ -1,19 +1,22 @@ import { Provider } from '@ethersproject/providers'; +import { BigNumber as BigNumberJs } from 'bignumber.js'; import { BigNumber, ethers } from 'ethers'; -import { ProtocolType, convertDecimals, objMap } from '@hyperlane-xyz/utils'; - import { - TOKEN_EXCHANGE_RATE_DECIMALS, - TOKEN_EXCHANGE_RATE_SCALE, -} from '../consts/igp.js'; + ProtocolType, + assert, + convertDecimals, + objMap, +} from '@hyperlane-xyz/utils'; + +import { getProtocolExchangeRateDecimals } from '../consts/igp.js'; import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; import { AgentCosmosGasPrice } from '../metadata/agentConfig.js'; import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; import { ChainMap, ChainName } from '../types.js'; import { getCosmosRegistryChain } from '../utils/cosmos.js'; -import { StorageGasOracleConfig } from './oracle/types.js'; +import { ProtocolAgnositicGasOracleConfig } from './oracle/types.js'; export interface GasPriceConfig { amount: string; @@ -101,55 +104,76 @@ export async function getCosmosChainGasPrice( }; } -// Gets the exchange rate of the remote quoted in local tokens -export function getTokenExchangeRateFromValues({ +// Gets the exchange rate of the remote quoted in local tokens, not accounting for decimals. +function getTokenExchangeRate({ local, remote, tokenPrices, exchangeRateMarginPct, - decimals, }: { local: ChainName; remote: ChainName; tokenPrices: ChainMap; exchangeRateMarginPct: number; - decimals: { local: number; remote: number }; -}): BigNumber { +}): BigNumberJs { // Workaround for chicken-egg dependency problem. // We need to provide some default value here to satisfy the config on initial load, // whilst knowing that it will get overwritten when a script actually gets run. const defaultValue = '1'; - const localValue = ethers.utils.parseUnits( - tokenPrices[local] ?? defaultValue, - TOKEN_EXCHANGE_RATE_DECIMALS, - ); - const remoteValue = ethers.utils.parseUnits( - tokenPrices[remote] ?? defaultValue, - TOKEN_EXCHANGE_RATE_DECIMALS, - ); + const localValue = new BigNumberJs(tokenPrices[local] ?? defaultValue); + const remoteValue = new BigNumberJs(tokenPrices[remote] ?? defaultValue); - // This does not yet account for decimals! - let exchangeRate = remoteValue.mul(TOKEN_EXCHANGE_RATE_SCALE).div(localValue); + // Note this does not account for decimals! + let exchangeRate = remoteValue.div(localValue); // Apply the premium - exchangeRate = exchangeRate.mul(100 + exchangeRateMarginPct).div(100); + exchangeRate = exchangeRate.times(100 + exchangeRateMarginPct).div(100); - return BigNumber.from( - convertDecimals(decimals.remote, decimals.local, exchangeRate.toString()), + assert( + exchangeRate.isGreaterThan(0), + 'Exchange rate must be greater than 0, possible loss of precision', ); + return exchangeRate; +} + +function getProtocolExchangeRate( + localProtocolType: ProtocolType, + exchangeRate: BigNumberJs, +): BigNumber { + const multiplierDecimals = getProtocolExchangeRateDecimals(localProtocolType); + const multiplier = new BigNumberJs(10).pow(multiplierDecimals); + const integer = exchangeRate + .times(multiplier) + .integerValue(BigNumberJs.ROUND_FLOOR) + .toString(10); + return BigNumber.from(integer); } // Gets the StorageGasOracleConfig for each remote chain for a particular local chain. // Accommodates small non-integer gas prices by scaling up the gas price // and scaling down the exchange rate by the same factor. +// A gasPriceModifier can be supplied to adjust the gas price based on a prospective +// gasOracleConfig. +// Values take into consideration the local chain's needs depending on the protocol type, +// e.g. the expected decimals of the token exchange rate, or whether to account for +// a native token decimal difference in the exchange rate. +// Therefore the values here can be applied directly to the chain's gas oracle. export function getLocalStorageGasOracleConfig({ local, + localProtocolType, gasOracleParams, exchangeRateMarginPct, + gasPriceModifier, }: { local: ChainName; + localProtocolType: ProtocolType; gasOracleParams: ChainMap; exchangeRateMarginPct: number; -}): ChainMap { + gasPriceModifier?: ( + local: ChainName, + remote: ChainName, + gasOracleConfig: ProtocolAgnositicGasOracleConfig, + ) => BigNumberJs.Value; +}): ChainMap { const remotes = Object.keys(gasOracleParams).filter( (remote) => remote !== local, ); @@ -160,57 +184,105 @@ export function getLocalStorageGasOracleConfig({ const localDecimals = gasOracleParams[local].nativeToken.decimals; return remotes.reduce((agg, remote) => { const remoteDecimals = gasOracleParams[remote].nativeToken.decimals; - let exchangeRate = getTokenExchangeRateFromValues({ + // The exchange rate, not yet accounting for decimals, and potentially + // floating point. + let exchangeRateFloat = getTokenExchangeRate({ local, remote, tokenPrices, exchangeRateMarginPct, - decimals: { local: localDecimals, remote: remoteDecimals }, }); - // First parse as a number, so we have floating point precision. + if (localProtocolType !== ProtocolType.Sealevel) { + // On all chains other than Sealevel, we need to adjust the exchange rate for decimals. + exchangeRateFloat = convertDecimals( + remoteDecimals, + localDecimals, + exchangeRateFloat, + ); + } + + // Make the exchange rate an integer by scaling it up by the appropriate factor for the protocol. + const exchangeRate = getProtocolExchangeRate( + localProtocolType, + exchangeRateFloat, + ); + + // First parse the gas price as a number, so we have floating point precision. // Recall it's possible to have gas prices that are not integers, even // after converting to the "wei" version of the token. - let gasPrice = - parseFloat(gasOracleParams[remote].gasPrice.amount) * - Math.pow(10, gasOracleParams[remote].gasPrice.decimals); - if (isNaN(gasPrice)) { + const gasPrice = new BigNumberJs( + gasOracleParams[remote].gasPrice.amount, + ).times(new BigNumberJs(10).pow(gasOracleParams[remote].gasPrice.decimals)); + if (gasPrice.isNaN()) { throw new Error( `Invalid gas price for chain ${remote}: ${gasOracleParams[remote].gasPrice.amount}`, ); } - // We have very little precision and ultimately need an integer value for - // the gas price that will be set on-chain. We scale up the gas price and - // scale down the exchange rate by the same factor. - if (gasPrice < 10 && gasPrice % 1 !== 0) { - // Scale up the gas price by 1e4 - const gasPriceScalingFactor = 1e4; - - // Check that there's no significant underflow when applying - // this to the exchange rate: - const adjustedExchangeRate = exchangeRate.div(gasPriceScalingFactor); - const recoveredExchangeRate = adjustedExchangeRate.mul( - gasPriceScalingFactor, - ); - if (recoveredExchangeRate.mul(100).div(exchangeRate).lt(99)) { - throw new Error('Too much underflow when downscaling exchange rate'); - } + // Get a prospective gasOracleConfig, adjusting the gas price and exchange rate + // as needed to account for precision loss (e.g. if the gas price is super small). + let gasOracleConfig = adjustForPrecisionLoss( + gasPrice, + exchangeRate, + remoteDecimals, + ); - // Apply the scaling factor - exchangeRate = adjustedExchangeRate; - gasPrice *= gasPriceScalingFactor; + // Apply the modifier if provided. + if (gasPriceModifier) { + // Once again adjust for precision loss after applying the modifier. + gasOracleConfig = adjustForPrecisionLoss( + gasPriceModifier(local, remote, gasOracleConfig), + BigNumber.from(gasOracleConfig.tokenExchangeRate), + remoteDecimals, + ); } - // Our integer gas price. - const gasPriceBn = BigNumber.from(Math.ceil(gasPrice)); - return { ...agg, - [remote]: { - tokenExchangeRate: exchangeRate.toString(), - gasPrice: gasPriceBn.toString(), - }, + [remote]: gasOracleConfig, }; - }, {} as ChainMap); + }, {} as ChainMap); +} + +function adjustForPrecisionLoss( + gasPrice: BigNumberJs.Value, + exchangeRate: BigNumber, + remoteDecimals: number, +): ProtocolAgnositicGasOracleConfig { + let newGasPrice = new BigNumberJs(gasPrice); + let newExchangeRate = exchangeRate; + + // We may have very little precision, and ultimately need an integer value for + // the gas price that will be set on-chain. If this is the case, we scale up the + // gas price and scale down the exchange rate by the same factor. + if (newGasPrice.lt(10) && newGasPrice.mod(1) !== new BigNumberJs(0)) { + // Scale up the gas price by 1e4 (arbitrary choice) + const gasPriceScalingFactor = 1e4; + + // Check that there's no significant underflow when applying + // this to the exchange rate: + const adjustedExchangeRate = newExchangeRate.div(gasPriceScalingFactor); + const recoveredExchangeRate = adjustedExchangeRate.mul( + gasPriceScalingFactor, + ); + if (recoveredExchangeRate.mul(100).div(newExchangeRate).lt(99)) { + throw new Error('Too much underflow when downscaling exchange rate'); + } + + newGasPrice = newGasPrice.times(gasPriceScalingFactor); + newExchangeRate = adjustedExchangeRate; + } + + const newGasPriceInteger = newGasPrice.integerValue(BigNumberJs.ROUND_CEIL); + assert( + newGasPriceInteger.gt(0), + 'Gas price must be greater than 0, possible loss of precision', + ); + + return { + tokenExchangeRate: newExchangeRate.toString(), + gasPrice: newGasPriceInteger.toString(), + tokenDecimals: remoteDecimals, + }; } diff --git a/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts b/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts index ae66d7adbf..2b31d62346 100644 --- a/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts +++ b/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts @@ -29,6 +29,7 @@ import { } from './types.js'; const hookTypes = Object.values(HookType); +const DEFAULT_TOKEN_DECIMALS = 18; function randomHookType(): HookType { // OP_STACK filtering is temporary until we have a way to deploy the required contracts @@ -99,6 +100,7 @@ function randomHookConfig( { tokenExchangeRate: randomInt(1234567891234).toString(), gasPrice: randomInt(1234567891234).toString(), + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, ]), ), @@ -347,18 +349,22 @@ describe('EvmHookModule', async () => { test1: { tokenExchangeRate: '1032586497157', gasPrice: '1026942205817', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test2: { tokenExchangeRate: '81451154935', gasPrice: '1231220057593', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test3: { tokenExchangeRate: '31347320275', gasPrice: '21944956734', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test4: { tokenExchangeRate: '1018619796544', gasPrice: '1124484183261', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, }, }, @@ -384,18 +390,22 @@ describe('EvmHookModule', async () => { test1: { tokenExchangeRate: '1132883204938', gasPrice: '1219466305935', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test2: { tokenExchangeRate: '938422264723', gasPrice: '229134538568', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test3: { tokenExchangeRate: '69699594189', gasPrice: '475781234236', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test4: { tokenExchangeRate: '1027245678936', gasPrice: '502686418976', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, }, }, @@ -417,18 +427,22 @@ describe('EvmHookModule', async () => { test1: { tokenExchangeRate: '443874625350', gasPrice: '799154764503', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test2: { tokenExchangeRate: '915348561750', gasPrice: '1124345797215', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test3: { tokenExchangeRate: '930832717805', gasPrice: '621743941770', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, test4: { tokenExchangeRate: '147394981623', gasPrice: '766494385983', + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, }, }, @@ -478,6 +492,7 @@ describe('EvmHookModule', async () => { { tokenExchangeRate: randomInt(1234567891234).toString(), gasPrice: randomInt(1234567891234).toString(), + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, ]), ), @@ -525,6 +540,7 @@ describe('EvmHookModule', async () => { { tokenExchangeRate: randomInt(987654321).toString(), gasPrice: randomInt(987654321).toString(), + tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, ]), ); diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index 45235304a6..6a7cfbbd3e 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -35,7 +35,7 @@ import { rootLogger, } from '@hyperlane-xyz/utils'; -import { TOKEN_EXCHANGE_RATE_SCALE } from '../consts/igp.js'; +import { TOKEN_EXCHANGE_RATE_SCALE_ETHEREUM } from '../consts/igp.js'; import { HyperlaneAddresses } from '../contracts/types.js'; import { HyperlaneModule, @@ -491,7 +491,7 @@ export class EvmHookModule extends HyperlaneModule< const exampleRemoteGasCost = BigNumber.from(target.tokenExchangeRate) .mul(target.gasPrice) .mul(exampleRemoteGas) - .div(TOKEN_EXCHANGE_RATE_SCALE); + .div(TOKEN_EXCHANGE_RATE_SCALE_ETHEREUM); this.logger.info( `${ this.chain diff --git a/typescript/sdk/src/hook/EvmHookReader.ts b/typescript/sdk/src/hook/EvmHookReader.ts index dae0f9d589..151d3d6f44 100644 --- a/typescript/sdk/src/hook/EvmHookReader.ts +++ b/typescript/sdk/src/hook/EvmHookReader.ts @@ -251,7 +251,8 @@ export class EvmHookReader extends HyperlaneReader implements HookReader { this.concurrency, domainIds, async (domainId) => { - const chainName = this.multiProvider.getChainName(domainId); + const { name: chainName, nativeToken } = + this.multiProvider.getChainMetadata(domainId); try { const { tokenExchangeRate, gasPrice } = await hook.getExchangeRateAndGasPrice(domainId); @@ -261,6 +262,7 @@ export class EvmHookReader extends HyperlaneReader implements HookReader { oracleConfig[chainName] = { tokenExchangeRate: tokenExchangeRate.toString(), gasPrice: gasPrice.toString(), + tokenDecimals: nativeToken?.decimals, }; const { gasOracle } = await hook.destinationGasConfigs(domainId); diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts index 768e93134b..76e749fd95 100644 --- a/typescript/sdk/src/hook/types.ts +++ b/typescript/sdk/src/hook/types.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { StorageGasOracleConfigSchema } from '../gas/oracle/types.js'; +import { ProtocolAgnositicGasOracleConfigSchema } from '../gas/oracle/types.js'; import { ZHash } from '../metadata/customZodTypes.js'; import { ChainMap, @@ -115,7 +115,7 @@ export const IgpSchema = OwnableSchema.extend({ beneficiary: z.string(), oracleKey: z.string(), overhead: z.record(z.number()), - oracleConfig: z.record(StorageGasOracleConfigSchema), + oracleConfig: z.record(ProtocolAgnositicGasOracleConfigSchema), }); export const DomainRoutingHookConfigSchema: z.ZodSchema = diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index ce71e653c6..0ec8e740b3 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -9,8 +9,10 @@ export { export { S3Config, S3Receipt, S3Wrapper } from './aws/s3.js'; export { S3Validator } from './aws/validator.js'; export { - TOKEN_EXCHANGE_RATE_DECIMALS, - TOKEN_EXCHANGE_RATE_SCALE, + TOKEN_EXCHANGE_RATE_DECIMALS_ETHEREUM, + TOKEN_EXCHANGE_RATE_SCALE_ETHEREUM, + getProtocolExchangeRateDecimals, + getProtocolExchangeRateScale, } from './consts/igp.js'; export { MAILBOX_VERSION } from './consts/mailbox.js'; export { @@ -129,6 +131,8 @@ export { HyperlaneIgpDeployer } from './gas/HyperlaneIgpDeployer.js'; export { StorageGasOracleConfig, StorageGasOracleConfigSchema, + ProtocolAgnositicGasOracleConfig, + ProtocolAgnositicGasOracleConfigSchema, } from './gas/oracle/types.js'; export { CoinGeckoTokenPriceGetter } from './gas/token-prices.js'; export { @@ -422,7 +426,6 @@ export { getCosmosChainGasPrice, getGasPrice, getLocalStorageGasOracleConfig, - getTokenExchangeRateFromValues, NativeTokenPriceConfig, } from './gas/utils.js'; export { GcpValidator } from './gcp/validator.js'; diff --git a/typescript/sdk/src/test/testUtils.ts b/typescript/sdk/src/test/testUtils.ts index 5a2d4f2baa..57ac9d3ecf 100644 --- a/typescript/sdk/src/test/testUtils.ts +++ b/typescript/sdk/src/test/testUtils.ts @@ -65,6 +65,7 @@ export function testCoreConfig( const TEST_ORACLE_CONFIG = { gasPrice: ethers.utils.parseUnits('1', 'gwei').toString(), tokenExchangeRate: ethers.utils.parseUnits('1', 10).toString(), + tokenDecimals: 18, }; const TEST_OVERHEAD_COST = 60000; diff --git a/typescript/sdk/src/warp/WarpCore.ts b/typescript/sdk/src/warp/WarpCore.ts index 1dfa2eac56..544fa24b8c 100644 --- a/typescript/sdk/src/warp/WarpCore.ts +++ b/typescript/sdk/src/warp/WarpCore.ts @@ -5,7 +5,7 @@ import { HexString, ProtocolType, assert, - convertDecimals, + convertDecimalsToIntegerString, convertToProtocolAddress, isValidAddress, isZeroishAddress, @@ -502,7 +502,7 @@ export class WarpCore { ); } - const destinationBalanceInOriginDecimals = convertDecimals( + const destinationBalanceInOriginDecimals = convertDecimalsToIntegerString( destinationToken.decimals, originToken.decimals, destinationBalance.toString(), @@ -679,7 +679,7 @@ export class WarpCore { // Convert the minDestinationTransferAmount to an origin amount const minOriginTransferAmount = destinationToken.amount( - convertDecimals( + convertDecimalsToIntegerString( originToken.decimals, destinationToken.decimals, minDestinationTransferAmount.toString(), diff --git a/typescript/utils/src/amount.ts b/typescript/utils/src/amount.ts index ab8a3b3347..2642ef1f73 100644 --- a/typescript/utils/src/amount.ts +++ b/typescript/utils/src/amount.ts @@ -111,25 +111,31 @@ export function eqAmountApproximate( * @param value The value to convert. * @returns `value` represented with `toDecimals` decimals in string type. */ -export function convertDecimals( +export function convertDecimalsToIntegerString( fromDecimals: number, toDecimals: number, value: BigNumber.Value, ): string { + const converted = convertDecimals(fromDecimals, toDecimals, value); + return converted.integerValue(BigNumber.ROUND_FLOOR).toString(10); +} + +export function convertDecimals( + fromDecimals: number, + toDecimals: number, + value: BigNumber.Value, +): BigNumber { const amount = BigNumber(value); - if (fromDecimals === toDecimals) return amount.toString(10); + if (fromDecimals === toDecimals) return amount; else if (fromDecimals > toDecimals) { const difference = fromDecimals - toDecimals; - return amount - .div(BigNumber(10).pow(difference)) - .integerValue(BigNumber.ROUND_FLOOR) - .toString(10); + return amount.div(BigNumber(10).pow(difference)); } // fromDecimals < toDecimals else { const difference = toDecimals - fromDecimals; - return amount.times(BigNumber(10).pow(difference)).toString(10); + return amount.times(BigNumber(10).pow(difference)); } } diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index f4bd9779cb..85b6d406e0 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -43,6 +43,7 @@ export { export { addBufferToGasLimit, convertDecimals, + convertDecimalsToIntegerString, eqAmountApproximate, fromWei, fromWeiRounded, From 85e16de623bd8c6cd99f71c914eaa92361b3542b Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:04:33 +0530 Subject: [PATCH 2/5] feat(release): relayer updated to #5065 (#5225) ### Description - updated image tag to `62702d3-20250121-002648` to update cursor_max_sequence behavior from #5178 ### Drive-by changes None ### Related issues None ### Backward compatibility Yes ### Testing Deployment --- typescript/infra/config/environments/mainnet3/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 94dcb97c82..47cb249672 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -655,7 +655,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '7546c01-20250120-171540', + tag: '62702d3-20250121-002648', }, blacklist, gasPaymentEnforcement: gasPaymentEnforcement, From c20406cd90b1a095f87a42c77fa56dd8101914dc Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:16:29 +0000 Subject: [PATCH 3/5] chore: treasure message blacklist (#5227) ### Description An arbitrum->treasure message was sent with a malformed recipient address, where the 20 bytes were right-padded instead of left-padded, causing a tx revert ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- typescript/infra/config/environments/mainnet3/agent.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 47cb249672..e6e4e8e69a 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -639,6 +639,9 @@ const blacklistedMessageIds = [ // test tx when route was first deployed, no merkle tree insertion // USDC/ethereum-inevm '0x998746dc822dc15332b8683fb8a29aec22ed3e2f2fb8245c40f56303c5cb6032', + + // malformed recipient in a warp transfer to `treasure` + '0xf20e3dc5172d824b146b91bb33d66532915fab605e44d2d76af7b5898a6390fe', ]; // Blacklist matching list intended to be used by all contexts. From 359ce5d82571d1ea27deac72c2416baecba12a99 Mon Sep 17 00:00:00 2001 From: Danil Nemirovsky Date: Tue, 21 Jan 2025 13:26:25 +0000 Subject: [PATCH 4/5] feat(release): Upgrade validators to latest version (#5192) ### Description Upgrade validators to latest version ### Backward compatibility Yes ### Testing Manual Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com> --- typescript/infra/config/environments/mainnet3/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index e6e4e8e69a..c740beb354 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -668,7 +668,7 @@ const hyperlane: RootAgentConfig = { validators: { docker: { repo, - tag: '95deca3-20250120-103609', + tag: '0372ff9-20250121-104245', }, rpcConsensusType: RpcConsensusType.Quorum, chains: validatorChainConfig(Contexts.Hyperlane), From e8fb7ef94571694e3e04d4eaa2e06ddbf1d41158 Mon Sep 17 00:00:00 2001 From: Paul Balaji <10051819+paulbalaji@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:16:08 +0000 Subject: [PATCH 5/5] feat: script to combine safe txs by chain id (#4965) ### Description feat: script to combine safe txs by chain id - pass in a directory of safe txs in json form - combines/groups transactions together by the chainid to submit to before: image after: image ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- typescript/infra/scripts/safes/combine-txs.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 typescript/infra/scripts/safes/combine-txs.ts diff --git a/typescript/infra/scripts/safes/combine-txs.ts b/typescript/infra/scripts/safes/combine-txs.ts new file mode 100644 index 0000000000..c6e284185d --- /dev/null +++ b/typescript/infra/scripts/safes/combine-txs.ts @@ -0,0 +1,92 @@ +// Import necessary modules +import { SafeTransaction } from '@safe-global/safe-core-sdk-types'; +// eslint-disable-next-line +import * as fs from 'fs'; +import * as path from 'path'; +import yargs from 'yargs'; + +import { rootLogger } from '@hyperlane-xyz/utils'; + +import { readJSONAtPath, writeJsonAtPath } from '../../src/utils/utils.js'; +import { getEnvironmentConfig } from '../core-utils.js'; + +type TxFile = { + version: string; + chainId: string; + meta: any; + transactions: SafeTransaction[]; +}; + +// Function to read and parse JSON files +function readJSONFiles(directory: string): Record { + const files = fs.readdirSync(directory); + const transactionsByChainId: Record = {}; + + for (const file of files) { + if (path.extname(file) === '.json') { + const filePath = path.join(directory, file); + const txs: TxFile = readJSONAtPath(filePath); + const chainId = txs.chainId; + if (!transactionsByChainId[chainId]) { + transactionsByChainId[chainId] = []; + } + transactionsByChainId[chainId].push(txs); + } + } + + return transactionsByChainId; +} + +// Function to write combined transactions to new JSON files +async function writeCombinedTransactions( + transactionsByChainId: Record, + directory: string, +) { + // Create the output directory + const outputDir = path.join(directory, `combined-txs-${Date.now()}`); + fs.mkdirSync(outputDir, { recursive: true }); + + const config = getEnvironmentConfig('mainnet3'); + const multiProvider = await config.getMultiProvider(); + + for (const [chainId, transactions] of Object.entries(transactionsByChainId)) { + // Create the output data + const outputData = { + version: '1.0', + chainId: chainId, + meta: {}, + transactions: transactions.flatMap((txFile) => txFile.transactions), + }; + + // Write the output file + // NOTE: hacky use of chainid instead of domainid or chain name here + const chainName = multiProvider.getChainName(chainId); + const outputFilePath = path.join(outputDir, `${chainId}-${chainName}.json`); + writeJsonAtPath(outputFilePath, outputData); + rootLogger.info(`Combined transactions written to ${outputFilePath}`); + } +} + +// Main function to execute the script +async function main() { + const { directory } = await yargs(process.argv.slice(2)).option('directory', { + type: 'string', + describe: 'directory containing txs', + demandOption: true, + alias: 'd', + }).argv; + + if (!fs.existsSync(directory)) { + rootLogger.error(`Directory ${directory} does not exist`); + process.exit(1); + } + + const transactionsByChainId = readJSONFiles(directory); + await writeCombinedTransactions(transactionsByChainId, directory); +} + +// Execute the main function and handle promise +main().catch((error) => { + rootLogger.error('An error occurred:', error); + process.exit(1); +});