From 176e710cae95ce866ced9c254a3ef6830cff840a Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Thu, 21 Mar 2024 13:50:23 +0000 Subject: [PATCH] Fix BSC gas price issues by treating chains with a base fee of 0 as non-1559 (#3462) ### Description - Fixes #3396 - See v2 backport here #3463 So turns out this is because: - BSC nodes tend to enforce a minimum gas price of 3 gwei when you submit a tx - it's possible for validators to include privileged txs at a lower gas price, so in practice (probably some mev stuff ?) there are a bunch of txs in blocks with transactions lower than 3 gwei - BSC's 1559 situation is they have a base fee of zero, so it's basically like they just use legacy txs - when we try to figure out what to pay in prio fees, we are suggested a 1 gwei priority fee: ``` $ cast rpc eth_feeHistory 10 latest "[5.0]" --rpc-url https://rpc.ankr.com/bsc {"oldestBlock":"0x23706f7","reward":[["0x3b9aca00"]],"baseFeePerGas":["0x0","0x0"],"gasUsedRatio":[0.1149265]} $ cast td 0x3b9aca00 1000000000 ``` So options are: 1. tx overrides for bsc, set it to 3 gwei 2. use a different eip 1559 estimator and set the percentile used in eth_feeHistory to something higher. Atm the percentile is 5%, if you change this to 50% we seem to get 3 gwei. This would have consequences for other chains tho 3. another heuristic, to fall back to legacy txs (where we can trust the eth_gasPrice to give us an accurate price) whenever the base fee is 0 I'm a tx override hater and we don't have this in the agents so far so I'm gonna go with 3. It's the least intrusive change --- rust/chains/hyperlane-ethereum/src/tx.rs | 63 ++++++++++++++++++- .../config/environments/mainnet3/agent.ts | 2 +- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/rust/chains/hyperlane-ethereum/src/tx.rs b/rust/chains/hyperlane-ethereum/src/tx.rs index 85e0edef0..f578a15f9 100644 --- a/rust/chains/hyperlane-ethereum/src/tx.rs +++ b/rust/chains/hyperlane-ethereum/src/tx.rs @@ -5,10 +5,17 @@ use std::time::Duration; use ethers::{ abi::Detokenize, prelude::{NameOrAddress, TransactionReceipt}, + providers::ProviderError, types::Eip1559TransactionRequest, }; use ethers_contract::builders::ContractCall; -use ethers_core::types::BlockNumber; +use ethers_core::{ + types::{BlockNumber, U256 as EthersU256}, + utils::{ + eip1559_default_estimator, EIP1559_FEE_ESTIMATION_PAST_BLOCKS, + EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE, + }, +}; use hyperlane_core::{utils::bytes_to_hex, ChainCommunicationError, ChainResult, H256, U256}; use tracing::{error, info}; @@ -84,10 +91,22 @@ where .saturating_add(U256::from(GAS_ESTIMATE_BUFFER).into()) .into() }; - let Ok((max_fee, max_priority_fee)) = provider.estimate_eip1559_fees(None).await else { + + let Ok((base_fee, max_fee, max_priority_fee)) = estimate_eip1559_fees(provider, None).await + else { // Is not EIP 1559 chain return Ok(tx.gas(gas_limit)); }; + + // If the base fee is zero, just treat the chain as a non-EIP-1559 chain. + // This is useful for BSC, where the base fee is zero, there's a minimum gas price + // generally enforced by nodes of 3 gwei, but EIP 1559 estimation suggests a priority + // fee lower than 3 gwei because of privileged transactions being included by block + // producers that have a lower priority fee. + if base_fee.is_zero() { + return Ok(tx.gas(gas_limit)); + } + // Is EIP 1559 chain let mut request = Eip1559TransactionRequest::new(); if let Some(from) = tx.tx.from() { @@ -109,6 +128,46 @@ where Ok(eip_1559_tx.gas(gas_limit)) } +type FeeEstimator = fn(EthersU256, Vec>) -> (EthersU256, EthersU256); + +/// Pretty much a copy of the logic in ethers-rs (https://github.com/hyperlane-xyz/ethers-rs/blob/c9ced035628da59376c369be035facda1648577a/ethers-providers/src/provider.rs#L478) +/// but returns the base fee as well as the max fee and max priority fee. +/// Gets a heuristic recommendation of max fee per gas and max priority fee per gas for +/// EIP-1559 compatible transactions. +async fn estimate_eip1559_fees( + provider: Arc, + estimator: Option, +) -> ChainResult<(EthersU256, EthersU256, EthersU256)> +where + M: Middleware + 'static, +{ + let base_fee_per_gas = provider + .get_block(BlockNumber::Latest) + .await + .map_err(ChainCommunicationError::from_other)? + .ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))? + .base_fee_per_gas + .ok_or_else(|| ProviderError::CustomError("EIP-1559 not activated".into()))?; + + let fee_history = provider + .fee_history( + EIP1559_FEE_ESTIMATION_PAST_BLOCKS, + BlockNumber::Latest, + &[EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await + .map_err(ChainCommunicationError::from_other)?; + + // use the provided fee estimator function, or fallback to the default implementation. + let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator { + es(base_fee_per_gas, fee_history.reward) + } else { + eip1559_default_estimator(base_fee_per_gas, fee_history.reward) + }; + + Ok((base_fee_per_gas, max_fee_per_gas, max_priority_fee_per_gas)) +} + pub(crate) async fn call_with_lag( call: ethers::contract::builders::ContractCall, provider: &M, diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index c6477c5ea..b13cb6ca1 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -137,7 +137,7 @@ const hyperlane: RootAgentConfig = { docker: { repo, // Includes Cosmos block-by-block indexing. - tag: 'a72c3cf-20240314-173418', + tag: '39df4ca-20240321-100543', }, gasPaymentEnforcement: [ // Temporary measure to ensure all inEVM warp route messages are delivered -