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
This commit is contained in:
Rohit Narurkar 2021-08-19 16:01:40 +02:00 committed by GitHub
parent 635236f061
commit ad779c867f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 233 additions and 10 deletions

View File

@ -399,6 +399,7 @@ mod eth_tests {
let _receipt = contract let _receipt = contract
.method::<_, H256>("setValue", "hi".to_owned()) .method::<_, H256>("setValue", "hi".to_owned())
.unwrap() .unwrap()
.legacy()
.send() .send()
.await .await
.unwrap() .unwrap()

View File

@ -36,6 +36,7 @@ pub use rlp;
use crate::types::{Address, Bytes, U256}; use crate::types::{Address, Bytes, U256};
use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey}; use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey};
use std::convert::TryInto; use std::convert::TryInto;
use std::ops::Neg;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -47,6 +48,19 @@ pub enum FormatBytes32StringError {
/// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei /// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei
pub const WEI_IN_ETHER: U256 = U256([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]); 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 /// Format the output for the user which prefer to see values
/// in ether (instead of wei) /// 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]) 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<Vec<U256>>) -> (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<Vec<U256>>) -> U256 {
let mut rewards: Vec<U256> = 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<i64> = 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. /// 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 /// 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 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<U256>> = 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<U256>> = 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());
}
} }

View File

@ -106,4 +106,8 @@ impl GasOracle for EthGasStation {
Ok(gas_price) Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
Err(GasOracleError::Eip1559EstimationNotSupported)
}
} }

View File

@ -77,4 +77,8 @@ impl GasOracle for Etherchain {
Ok(gas_price) Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
Err(GasOracleError::Eip1559EstimationNotSupported)
}
} }

View File

@ -108,4 +108,8 @@ impl GasOracle for Etherscan {
_ => Err(GasOracleError::GasCategoryNotSupported), _ => Err(GasOracleError::GasCategoryNotSupported),
} }
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
Err(GasOracleError::Eip1559EstimationNotSupported)
}
} }

View File

@ -80,4 +80,8 @@ impl GasOracle for GasNow {
Ok(gas_price) Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
Err(GasOracleError::Eip1559EstimationNotSupported)
}
} }

View File

@ -59,6 +59,13 @@ where
Ok(self.gas_oracle.fetch().await?) Ok(self.gas_oracle.fetch().await?)
} }
async fn estimate_eip1559_fees(
&self,
_: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
) -> Result<(U256, U256), Self::Error> {
Ok(self.gas_oracle.estimate_eip1559_fees().await?)
}
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>( async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self, &self,
tx: T, tx: T,
@ -77,8 +84,17 @@ where
inner.tx.gas_price = Some(self.get_gas_price().await?); inner.tx.gas_price = Some(self.get_gas_price().await?);
} }
} }
TypedTransaction::Eip1559(_) => { TypedTransaction::Eip1559(ref mut inner) => {
return Err(MiddlewareError::UnsupportedTxType); 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 self.inner

View File

@ -42,6 +42,9 @@ pub enum GasOracleError {
/// supported by the gas oracle API /// supported by the gas oracle API
#[error("gas category not supported")] #[error("gas category not supported")]
GasCategoryNotSupported, GasCategoryNotSupported,
#[error("EIP-1559 gas estimation not supported")]
Eip1559EstimationNotSupported,
} }
/// `GasOracle` is a trait that an underlying gas oracle needs to implement. /// `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<U256, GasOracleError>; async fn fetch(&self) -> Result<U256, GasOracleError>;
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError>;
} }

View File

@ -265,15 +265,19 @@ pub trait Middleware: Sync + Send + Debug {
inner.from = self.default_sender(); inner.from = self.default_sender();
} }
let (max_priority_fee_per_gas, max_fee_per_gas, gas) = futures_util::try_join!( let gas = maybe(inner.gas, self.estimate_gas(&tx_clone)).await?;
// 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)),
)?;
inner.gas = Some(gas); 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) self.inner().get_gas_price().await.map_err(FromErr::from)
} }
async fn estimate_eip1559_fees(
&self,
estimator: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
) -> Result<(U256, U256), Self::Error> {
self.inner()
.estimate_eip1559_fees(estimator)
.await
.map_err(FromErr::from)
}
async fn get_accounts(&self) -> Result<Vec<Address>, Self::Error> { async fn get_accounts(&self) -> Result<Vec<Address>, Self::Error> {
self.inner().get_accounts().await.map_err(FromErr::from) self.inner().get_accounts().await.map_err(FromErr::from)
} }

View File

@ -261,6 +261,37 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
self.request("eth_gasPrice", ()).await 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<fn(U256, Vec<Vec<U256>>) -> (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 /// Gets the accounts on the node
async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> { async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> {
self.request("eth_accounts", ()).await self.request("eth_accounts", ()).await

View File

@ -199,6 +199,17 @@ mod eth_tests {
.into(); .into();
check_tx(&provider, tx, 2).await; check_tx(&provider, tx, 2).await;
} }
#[tokio::test]
async fn eip1559_fee_estimation() {
let provider = Provider::<Http>::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")] #[cfg(feature = "celo")]