From ad779c867fa0976cbaab28aa3885601bb8ea53ed Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Thu, 19 Aug 2021 16:01:40 +0200 Subject: [PATCH] feat: fee estimation with custom/default fn (#369) * feat: fee estimation with custom/default fn * fix: clippy and percentage calc fix * calculate priority fee only if base fee above threshold * chore: some comments * fix: use legacy tx for ganache * test: test a few cases for fee estimation using utils --- ethers-contract/tests/contract.rs | 1 + ethers-core/src/utils/mod.rs | 129 ++++++++++++++++++ .../src/gas_oracle/eth_gas_station.rs | 4 + .../src/gas_oracle/etherchain.rs | 4 + ethers-middleware/src/gas_oracle/etherscan.rs | 4 + ethers-middleware/src/gas_oracle/gas_now.rs | 4 + .../src/gas_oracle/middleware.rs | 20 ++- ethers-middleware/src/gas_oracle/mod.rs | 5 + ethers-providers/src/lib.rs | 30 ++-- ethers-providers/src/provider.rs | 31 +++++ ethers-providers/tests/provider.rs | 11 ++ 11 files changed, 233 insertions(+), 10 deletions(-) diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 75a4afac..8e2008e1 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -399,6 +399,7 @@ mod eth_tests { let _receipt = contract .method::<_, H256>("setValue", "hi".to_owned()) .unwrap() + .legacy() .send() .await .unwrap() diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs index 5717468a..8809187f 100644 --- a/ethers-core/src/utils/mod.rs +++ b/ethers-core/src/utils/mod.rs @@ -36,6 +36,7 @@ pub use rlp; use crate::types::{Address, Bytes, U256}; use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey}; use std::convert::TryInto; +use std::ops::Neg; use thiserror::Error; #[derive(Debug, Error)] @@ -47,6 +48,19 @@ pub enum FormatBytes32StringError { /// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei pub const WEI_IN_ETHER: U256 = U256([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]); +/// The number of blocks from the past for which the fee rewards are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10; +/// The default percentile of gas premiums that are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE: f64 = 5.0; +/// The default max priority fee per gas, used in case the base fee is within a threshold. +pub const EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE: u64 = 3_000_000_000; +/// The threshold for base fee below which we use the default priority fee, and beyond which we +/// estimate an appropriate value for priority fee. +pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000; +/// The threshold max change/difference (in %) at which we will ignore the fee history values +/// under it. +pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200; + /// Format the output for the user which prefer to see values /// in ether (instead of wei) /// @@ -187,6 +201,92 @@ pub fn parse_bytes32_string(bytes: &[u8; 32]) -> Result<&str, std::str::Utf8Erro std::str::from_utf8(&bytes[..length]) } +/// The default EIP-1559 fee estimator which is based on the work by [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts) +pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: Vec>) -> (U256, U256) { + let max_priority_fee_per_gas = + if base_fee_per_gas < U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) { + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE) + } else { + std::cmp::max( + estimate_priority_fee(rewards), + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE), + ) + }; + let potential_max_fee = base_fee_surged(base_fee_per_gas); + let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee { + max_priority_fee_per_gas + potential_max_fee + } else { + potential_max_fee + }; + (max_fee_per_gas, max_priority_fee_per_gas) +} + +fn estimate_priority_fee(rewards: Vec>) -> U256 { + let mut rewards: Vec = rewards + .iter() + .map(|r| r[0]) + .filter(|r| *r > U256::zero()) + .collect(); + if rewards.is_empty() { + return U256::zero(); + } + if rewards.len() == 1 { + return rewards[0]; + } + // Sort the rewards as we will eventually take the median. + rewards.sort(); + + // A copy of the same vector is created for convenience to calculate percentage change + // between subsequent fee values. + let mut rewards_copy = rewards.clone(); + rewards_copy.rotate_left(1); + + let mut percentage_change: Vec = rewards + .iter() + .zip(rewards_copy.iter()) + .map(|(a, b)| { + if b > a { + ((b - a).low_u32() as i64 * 100) / (a.low_u32() as i64) + } else { + (((a - b).low_u32() as i64 * 100) / (a.low_u32() as i64)).neg() + } + }) + .collect(); + percentage_change.pop(); + + // Fetch the max of the percentage change, and that element's index. + let max_change = percentage_change.iter().max().unwrap(); + let max_change_index = percentage_change + .iter() + .position(|&c| c == *max_change) + .unwrap(); + + // If we encountered a big change in fees at a certain position, then consider only + // the values >= it. + let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE + && (max_change_index >= (rewards.len() / 2)) + { + rewards[max_change_index..].to_vec() + } else { + rewards + }; + + // Return the median. + values[values.len() / 2] +} + +fn base_fee_surged(base_fee_per_gas: U256) -> U256 { + if base_fee_per_gas <= U256::from(40_000_000_000u64) { + base_fee_per_gas * 2 + } else if base_fee_per_gas <= U256::from(100_000_000_000u64) { + base_fee_per_gas * 16 / 10 + } else if base_fee_per_gas <= U256::from(200_000_000_000u64) { + base_fee_per_gas * 14 / 10 + } else { + base_fee_per_gas * 12 / 10 + } +} + /// A bit of hack to find an unused TCP port. /// /// Does not guarantee that the given port is unused after the function exists, just that it was @@ -451,4 +551,33 @@ mod tests { FormatBytes32StringError::TextTooLong )); } + + #[test] + fn test_eip1559_default_estimator() { + // If the base fee is below the triggering base fee, we should get the default priority fee + // with the base fee surged. + let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) - 1; + let rewards: Vec> = vec![vec![]]; + let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards); + assert_eq!( + priority_fee, + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE) + ); + assert_eq!(base_fee, base_fee_surged(base_fee_per_gas)); + + // If the base fee is above the triggering base fee, we calculate the priority fee using + // the fee history (rewards). + let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) + 1; + let rewards: Vec> = vec![ + vec![100_000_000_000u64.into()], + vec![105_000_000_000u64.into()], + vec![102_000_000_000u64.into()], + ]; // say, last 3 blocks + let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards.clone()); + assert_eq!(base_fee, base_fee_surged(base_fee_per_gas)); + assert_eq!(priority_fee, estimate_priority_fee(rewards.clone())); + + // The median should be taken because none of the changes are big enough to ignore values. + assert_eq!(estimate_priority_fee(rewards), 102_000_000_000u64.into()); + } } diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs index 12386a6d..f8f0e6d4 100644 --- a/ethers-middleware/src/gas_oracle/eth_gas_station.rs +++ b/ethers-middleware/src/gas_oracle/eth_gas_station.rs @@ -106,4 +106,8 @@ impl GasOracle for EthGasStation { Ok(gas_price) } + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + Err(GasOracleError::Eip1559EstimationNotSupported) + } } diff --git a/ethers-middleware/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs index e114353f..89ecca27 100644 --- a/ethers-middleware/src/gas_oracle/etherchain.rs +++ b/ethers-middleware/src/gas_oracle/etherchain.rs @@ -77,4 +77,8 @@ impl GasOracle for Etherchain { Ok(gas_price) } + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + Err(GasOracleError::Eip1559EstimationNotSupported) + } } diff --git a/ethers-middleware/src/gas_oracle/etherscan.rs b/ethers-middleware/src/gas_oracle/etherscan.rs index e72b9cfa..658516b0 100644 --- a/ethers-middleware/src/gas_oracle/etherscan.rs +++ b/ethers-middleware/src/gas_oracle/etherscan.rs @@ -108,4 +108,8 @@ impl GasOracle for Etherscan { _ => Err(GasOracleError::GasCategoryNotSupported), } } + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + Err(GasOracleError::Eip1559EstimationNotSupported) + } } diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs index a39161e5..2bf6a049 100644 --- a/ethers-middleware/src/gas_oracle/gas_now.rs +++ b/ethers-middleware/src/gas_oracle/gas_now.rs @@ -80,4 +80,8 @@ impl GasOracle for GasNow { Ok(gas_price) } + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + Err(GasOracleError::Eip1559EstimationNotSupported) + } } diff --git a/ethers-middleware/src/gas_oracle/middleware.rs b/ethers-middleware/src/gas_oracle/middleware.rs index d77f3262..40ce3b5f 100644 --- a/ethers-middleware/src/gas_oracle/middleware.rs +++ b/ethers-middleware/src/gas_oracle/middleware.rs @@ -59,6 +59,13 @@ where Ok(self.gas_oracle.fetch().await?) } + async fn estimate_eip1559_fees( + &self, + _: Option>) -> (U256, U256)>, + ) -> Result<(U256, U256), Self::Error> { + Ok(self.gas_oracle.estimate_eip1559_fees().await?) + } + async fn send_transaction + Send + Sync>( &self, tx: T, @@ -77,8 +84,17 @@ where inner.tx.gas_price = Some(self.get_gas_price().await?); } } - TypedTransaction::Eip1559(_) => { - return Err(MiddlewareError::UnsupportedTxType); + TypedTransaction::Eip1559(ref mut inner) => { + if inner.max_priority_fee_per_gas.is_none() || inner.max_fee_per_gas.is_none() { + let (max_fee_per_gas, max_priority_fee_per_gas) = + self.estimate_eip1559_fees(None).await?; + if inner.max_priority_fee_per_gas.is_none() { + inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); + } + if inner.max_fee_per_gas.is_none() { + inner.max_fee_per_gas = Some(max_fee_per_gas); + } + } } }; self.inner diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs index f6a94850..603ada92 100644 --- a/ethers-middleware/src/gas_oracle/mod.rs +++ b/ethers-middleware/src/gas_oracle/mod.rs @@ -42,6 +42,9 @@ pub enum GasOracleError { /// supported by the gas oracle API #[error("gas category not supported")] GasCategoryNotSupported, + + #[error("EIP-1559 gas estimation not supported")] + Eip1559EstimationNotSupported, } /// `GasOracle` is a trait that an underlying gas oracle needs to implement. @@ -80,4 +83,6 @@ pub trait GasOracle: Send + Sync + std::fmt::Debug { /// # } /// ``` async fn fetch(&self) -> Result; + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError>; } diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index c09d494f..43b5f120 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -265,15 +265,19 @@ pub trait Middleware: Sync + Send + Debug { inner.from = self.default_sender(); } - let (max_priority_fee_per_gas, max_fee_per_gas, gas) = futures_util::try_join!( - // TODO: Replace with algorithms using eth_feeHistory - maybe(inner.max_priority_fee_per_gas, self.get_gas_price()), - maybe(inner.max_fee_per_gas, self.get_gas_price()), - maybe(inner.gas, self.estimate_gas(&tx_clone)), - )?; + let gas = maybe(inner.gas, self.estimate_gas(&tx_clone)).await?; inner.gas = Some(gas); - inner.max_fee_per_gas = Some(max_fee_per_gas); - inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); + + if inner.max_fee_per_gas.is_none() || inner.max_priority_fee_per_gas.is_none() { + let (max_fee_per_gas, max_priority_fee_per_gas) = + self.estimate_eip1559_fees(None).await?; + if inner.max_fee_per_gas.is_none() { + inner.max_fee_per_gas = Some(max_fee_per_gas); + } + if inner.max_priority_fee_per_gas.is_none() { + inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); + } + } } }; @@ -401,6 +405,16 @@ pub trait Middleware: Sync + Send + Debug { self.inner().get_gas_price().await.map_err(FromErr::from) } + async fn estimate_eip1559_fees( + &self, + estimator: Option>) -> (U256, U256)>, + ) -> Result<(U256, U256), Self::Error> { + self.inner() + .estimate_eip1559_fees(estimator) + .await + .map_err(FromErr::from) + } + async fn get_accounts(&self) -> Result, Self::Error> { self.inner().get_accounts().await.map_err(FromErr::from) } diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 8e61b3ab..f10d1ea6 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -261,6 +261,37 @@ impl Middleware for Provider

{ self.request("eth_gasPrice", ()).await } + /// 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( + &self, + estimator: Option>) -> (U256, U256)>, + ) -> Result<(U256, U256), Self::Error> { + let base_fee_per_gas = self + .get_block(BlockNumber::Latest) + .await? + .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 = self + .fee_history( + utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS, + BlockNumber::Latest, + &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await?; + + // 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 { + utils::eip1559_default_estimator(base_fee_per_gas, fee_history.reward) + }; + + Ok((max_fee_per_gas, max_priority_fee_per_gas)) + } + /// Gets the accounts on the node async fn get_accounts(&self) -> Result, ProviderError> { self.request("eth_accounts", ()).await diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs index f3390967..62edd568 100644 --- a/ethers-providers/tests/provider.rs +++ b/ethers-providers/tests/provider.rs @@ -199,6 +199,17 @@ mod eth_tests { .into(); check_tx(&provider, tx, 2).await; } + + #[tokio::test] + async fn eip1559_fee_estimation() { + let provider = Provider::::try_from( + "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", + ) + .unwrap(); + + let (_max_fee_per_gas, _max_priority_fee_per_gas) = + provider.estimate_eip1559_fees(None).await.unwrap(); + } } #[cfg(feature = "celo")]