Selaa lähdekoodia

[fortuna] Adjust gas oracle logic to eliminate ethers.rs priority fee lower bound (#1596)

* use gas oracle

* jeez

* cleanup

* gr

* format

* fix comment

* gr
Jayant Krishnamurthy 1 vuosi sitten
vanhempi
sitoutus
98c1fcc40c

+ 1 - 1
apps/fortuna/Cargo.lock

@@ -1488,7 +1488,7 @@ dependencies = [
 
 [[package]]
 name = "fortuna"
-version = "5.3.2"
+version = "5.3.3"
 dependencies = [
  "anyhow",
  "axum",

+ 1 - 1
apps/fortuna/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name    = "fortuna"
-version = "5.3.2"
+version = "5.3.3"
 edition = "2021"
 
 [dependencies]

+ 1 - 0
apps/fortuna/src/chain.rs

@@ -1,2 +1,3 @@
+pub(crate) mod eth_gas_oracle;
 pub(crate) mod ethereum;
 pub(crate) mod reader;

+ 161 - 0
apps/fortuna/src/chain/eth_gas_oracle.rs

@@ -0,0 +1,161 @@
+use {
+    axum::async_trait,
+    ethers::{
+        prelude::{
+            gas_oracle::{
+                GasOracleError,
+                Result,
+            },
+            GasOracle,
+        },
+        providers::Middleware,
+        types::{
+            I256,
+            U256,
+        },
+    },
+};
+
+// The default fee estimation logic in ethers.rs includes some hardcoded constants that do not
+// work well in layer 2 networks because it lower bounds the priority fee at 3 gwei.
+// Unfortunately this logic is not configurable in ethers.rs.
+//
+// Thus, this file is copy-pasted from places in ethers.rs with all of the fee constants divided by 1000000.
+// See original logic here:
+// https://github.com/gakonst/ethers-rs/blob/master/ethers-providers/src/rpc/provider.rs#L452
+
+/// 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;
+/// 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;
+
+/// Thresholds at which the base fee gets a multiplier
+pub const SURGE_THRESHOLD_1: u64 = 40_000;
+pub const SURGE_THRESHOLD_2: u64 = 100_000;
+pub const SURGE_THRESHOLD_3: u64 = 200_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;
+
+
+/// Gas oracle from a [`Middleware`] implementation such as an
+/// Ethereum RPC provider.
+#[derive(Clone, Debug)]
+#[must_use]
+pub struct EthProviderOracle<M: Middleware> {
+    provider: M,
+}
+
+impl<M: Middleware> EthProviderOracle<M> {
+    pub fn new(provider: M) -> Self {
+        Self { provider }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl<M: Middleware> GasOracle for EthProviderOracle<M>
+where
+    M::Error: 'static,
+{
+    async fn fetch(&self) -> Result<U256> {
+        self.provider
+            .get_gas_price()
+            .await
+            .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
+    }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
+        self.provider
+            .estimate_eip1559_fees(Some(eip1559_default_estimator))
+            .await
+            .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
+    }
+}
+
+/// 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)
+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<I256> = rewards
+        .iter()
+        .zip(rewards_copy.iter())
+        .map(|(a, b)| {
+            let a = I256::try_from(*a).expect("priority fee overflow");
+            let b = I256::try_from(*b).expect("priority fee overflow");
+            ((b - a) * 100) / a
+        })
+        .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.into()
+        && (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(SURGE_THRESHOLD_1) {
+        base_fee_per_gas * 2
+    } else if base_fee_per_gas <= U256::from(SURGE_THRESHOLD_2) {
+        base_fee_per_gas * 16 / 10
+    } else if base_fee_per_gas <= U256::from(SURGE_THRESHOLD_3) {
+        base_fee_per_gas * 14 / 10
+    } else {
+        base_fee_per_gas * 12 / 10
+    }
+}

+ 24 - 12
apps/fortuna/src/chain/ethereum.rs

@@ -1,11 +1,14 @@
 use {
     crate::{
-        chain::reader::{
-            self,
-            BlockNumber,
-            BlockStatus,
-            EntropyReader,
-            RequestedWithCallbackEvent,
+        chain::{
+            eth_gas_oracle::EthProviderOracle,
+            reader::{
+                self,
+                BlockNumber,
+                BlockStatus,
+                EntropyReader,
+                RequestedWithCallbackEvent,
+            },
         },
         config::EthereumConfig,
     },
@@ -24,6 +27,7 @@ use {
         },
         core::types::Address,
         middleware::{
+            gas_oracle::GasOracleMiddleware,
             transformer::{
                 Transformer,
                 TransformerError,
@@ -63,9 +67,12 @@ abigen!(
 );
 
 pub type SignablePythContract = PythRandom<
-    TransformerMiddleware<
-        NonceManagerMiddleware<SignerMiddleware<Provider<Http>, LocalWallet>>,
-        LegacyTxTransformer,
+    GasOracleMiddleware<
+        TransformerMiddleware<
+            NonceManagerMiddleware<SignerMiddleware<Provider<Http>, LocalWallet>>,
+            LegacyTxTransformer,
+        >,
+        EthProviderOracle<Provider<Http>>,
     >,
 >;
 pub type PythContract = PythRandom<Provider<Http>>;
@@ -96,6 +103,8 @@ impl SignablePythContract {
         let provider = Provider::<Http>::try_from(&chain_config.geth_rpc_addr)?;
         let chain_id = provider.get_chainid().await?;
 
+        let gas_oracle = EthProviderOracle::new(provider.clone());
+
         let transformer = LegacyTxTransformer {
             use_legacy_tx: chain_config.legacy_tx,
         };
@@ -108,9 +117,12 @@ impl SignablePythContract {
 
         Ok(PythRandom::new(
             chain_config.contract_addr,
-            Arc::new(TransformerMiddleware::new(
-                NonceManagerMiddleware::new(SignerMiddleware::new(provider, wallet__), address),
-                transformer,
+            Arc::new(GasOracleMiddleware::new(
+                TransformerMiddleware::new(
+                    NonceManagerMiddleware::new(SignerMiddleware::new(provider, wallet__), address),
+                    transformer,
+                ),
+                gas_oracle,
             )),
         ))
     }

+ 2 - 1
apps/fortuna/src/keeper.rs

@@ -223,7 +223,7 @@ pub async fn run_keeper_threads(
             .await
             .expect("Chain config should be valid"),
     );
-    let keeper_address = contract.client().inner().inner().signer().address();
+    let keeper_address = contract.client().inner().inner().inner().signer().address();
 
     let fulfilled_requests_cache = Arc::new(RwLock::new(HashMap::<u64, RequestState>::new()));
 
@@ -430,6 +430,7 @@ pub async fn process_event(
                                             .client()
                                             .inner()
                                             .inner()
+                                            .inner()
                                             .signer()
                                             .address()
                                             .to_string(),