fix: cap agent EVM tx limit to the latest block gas limit (#4489)

### Description

- Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4239
- I think ideally we'd maybe cache this, but there isn't a super
accessible way of doing this atm and we need this ASAP to accommodate
Everclear. So personally am happy to take on the extra RPC per
transaction and at some point we'll likely want to revisit our tx
submission efficiency, plus we end up needing the latest block for eip
1559 estimation anyways

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

<!--
What kind of testing have these changes undergone?

None/Manual/Unit Tests
-->
pull/4502/head
Trevor Porter 2 months ago committed by GitHub
parent a8f0ccd14f
commit 78d6fcf5aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 80
      rust/main/chains/hyperlane-ethereum/src/contracts/mailbox.rs
  2. 32
      rust/main/chains/hyperlane-ethereum/src/tx.rs

@ -619,8 +619,12 @@ mod test {
/// An amount of gas to add to the estimated gas
const GAS_ESTIMATE_BUFFER: u32 = 75_000;
#[tokio::test]
async fn test_process_estimate_costs_sets_l2_gas_limit_for_arbitrum() {
fn get_test_mailbox(
domain: HyperlaneDomain,
) -> (
EthereumMailbox<Provider<Arc<MockProvider>>>,
Arc<MockProvider>,
) {
let mock_provider = Arc::new(MockProvider::new());
let provider = Arc::new(Provider::new(mock_provider.clone()));
let connection_conf = ConnectionConf {
@ -635,12 +639,19 @@ mod test {
provider.clone(),
&connection_conf,
&ContractLocator {
// An Arbitrum Nitro chain
domain: &HyperlaneDomain::Known(KnownHyperlaneDomain::PlumeTestnet),
domain: &domain,
// Address doesn't matter because we're using a MockProvider
address: H256::default(),
},
);
(mailbox, mock_provider)
}
#[tokio::test]
async fn test_process_estimate_costs_sets_l2_gas_limit_for_arbitrum() {
// An Arbitrum Nitro chain
let (mailbox, mock_provider) =
get_test_mailbox(HyperlaneDomain::Known(KnownHyperlaneDomain::PlumeTestnet));
let message = HyperlaneMessage::default();
let metadata: Vec<u8> = vec![];
@ -662,12 +673,17 @@ mod test {
EthersU256::from(ethers::utils::parse_units("15", "gwei").unwrap()).into();
mock_provider.push(gas_price).unwrap();
// RPC 3: eth_estimateGas to the ArbitrumNodeInterface's estimateRetryableTicket function by process_estimate_costs
// RPC 4: eth_estimateGas to the ArbitrumNodeInterface's estimateRetryableTicket function by process_estimate_costs
let l2_gas_limit = U256::from(200000); // 200k gas
mock_provider.push(l2_gas_limit).unwrap();
// RPC 2: eth_getBlockByNumber from the estimate_eip1559_fees call in process_contract_call
mock_provider.push(Block::<Transaction>::default()).unwrap();
let latest_block: Block<Transaction> = Block {
gas_limit: ethers::types::U256::MAX,
..Block::<Transaction>::default()
};
// RPC 3: eth_getBlockByNumber from the fill_tx_gas_params call in process_contract_call
// to get the latest block gas limit and for eip 1559 fee estimation
mock_provider.push(latest_block).unwrap();
// RPC 1: eth_estimateGas from the estimate_gas call in process_contract_call
// Return 1M gas
@ -679,7 +695,7 @@ mod test {
.await
.unwrap();
// The TxCostEstimat's gas limit includes the buffer
// The TxCostEstimate's gas limit includes the buffer
let estimated_gas_limit = gas_limit.saturating_add(GAS_ESTIMATE_BUFFER.into());
assert_eq!(
@ -691,4 +707,52 @@ mod test {
},
);
}
#[tokio::test]
async fn test_tx_gas_limit_caps_at_block_gas_limit() {
let (mailbox, mock_provider) =
get_test_mailbox(HyperlaneDomain::Known(KnownHyperlaneDomain::Ethereum));
let message = HyperlaneMessage::default();
let metadata: Vec<u8> = vec![];
// The MockProvider responses we push are processed in LIFO
// order, so we start with the final RPCs and work toward the first
// RPCs
// RPC 4: eth_gasPrice by process_estimate_costs
// Return 15 gwei
let gas_price: U256 =
EthersU256::from(ethers::utils::parse_units("15", "gwei").unwrap()).into();
mock_provider.push(gas_price).unwrap();
let latest_block_gas_limit = U256::from(12345u32);
let latest_block: Block<Transaction> = Block {
gas_limit: latest_block_gas_limit.into(),
..Block::<Transaction>::default()
};
// RPC 3: eth_getBlockByNumber from the fill_tx_gas_params call in process_contract_call
// to get the latest block gas limit and for eip 1559 fee estimation
mock_provider.push(latest_block).unwrap();
// RPC 1: eth_estimateGas from the estimate_gas call in process_contract_call
// Return 1M gas
let gas_limit = U256::from(1000000u32);
mock_provider.push(gas_limit).unwrap();
let tx_cost_estimate = mailbox
.process_estimate_costs(&message, &metadata)
.await
.unwrap();
assert_eq!(
tx_cost_estimate,
TxCostEstimate {
// The block gas limit is the cap
gas_limit: latest_block_gas_limit,
gas_price: gas_price.try_into().unwrap(),
l2_gas_limit: None,
},
);
}
}

@ -6,7 +6,7 @@ use ethers::{
abi::Detokenize,
prelude::{NameOrAddress, TransactionReceipt},
providers::{JsonRpcClient, PendingTransaction, ProviderError},
types::Eip1559TransactionRequest,
types::{Block, Eip1559TransactionRequest, TxHash},
};
use ethers_contract::builders::ContractCall;
use ethers_core::{
@ -17,7 +17,7 @@ use ethers_core::{
},
};
use hyperlane_core::{utils::bytes_to_hex, ChainCommunicationError, ChainResult, H256, U256};
use tracing::{debug, error, info};
use tracing::{debug, error, info, warn};
use crate::{Middleware, TransactionOverrides};
@ -107,6 +107,24 @@ where
} else {
estimated_gas_limit
};
// Cap the gas limit to the block gas limit
let latest_block = provider
.get_block(BlockNumber::Latest)
.await
.map_err(ChainCommunicationError::from_other)?
.ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))?;
let block_gas_limit: U256 = latest_block.gas_limit.into();
let gas_limit = if gas_limit > block_gas_limit {
warn!(
?gas_limit,
?block_gas_limit,
"Gas limit for transaction is higher than the block gas limit. Capping it to the block gas limit."
);
block_gas_limit
} else {
gas_limit
};
debug!(?estimated_gas_limit, gas_override=?transaction_overrides.gas_limit, used_gas_limit=?gas_limit, "Gas limit set for transaction");
if let Some(gas_price) = transaction_overrides.gas_price {
@ -114,7 +132,8 @@ where
return Ok(tx.gas_price(gas_price).gas(gas_limit));
}
let Ok((base_fee, max_fee, max_priority_fee)) = estimate_eip1559_fees(provider, None).await
let Ok((base_fee, max_fee, max_priority_fee)) =
estimate_eip1559_fees(provider, None, &latest_block).await
else {
// Is not EIP 1559 chain
return Ok(tx.gas(gas_limit));
@ -169,15 +188,12 @@ type FeeEstimator = fn(EthersU256, Vec<Vec<EthersU256>>) -> (EthersU256, EthersU
async fn estimate_eip1559_fees<M>(
provider: Arc<M>,
estimator: Option<FeeEstimator>,
latest_block: &Block<TxHash>,
) -> 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()))?
let base_fee_per_gas = latest_block
.base_fee_per_gas
.ok_or_else(|| ProviderError::CustomError("EIP-1559 not activated".into()))?;

Loading…
Cancel
Save