From 98c1fcc40c80a6ab6d760ac00e4ae831ff3eb31a Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 22 May 2024 20:18:05 -0700 Subject: [PATCH] [fortuna] Adjust gas oracle logic to eliminate ethers.rs priority fee lower bound (#1596) * use gas oracle * jeez * cleanup * gr * format * fix comment * gr --- apps/fortuna/Cargo.lock | 2 +- apps/fortuna/Cargo.toml | 2 +- apps/fortuna/src/chain.rs | 1 + apps/fortuna/src/chain/eth_gas_oracle.rs | 161 +++++++++++++++++++++++ apps/fortuna/src/chain/ethereum.rs | 36 +++-- apps/fortuna/src/keeper.rs | 3 +- 6 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 apps/fortuna/src/chain/eth_gas_oracle.rs diff --git a/apps/fortuna/Cargo.lock b/apps/fortuna/Cargo.lock index a190fe0882..73d5ac0b50 100644 --- a/apps/fortuna/Cargo.lock +++ b/apps/fortuna/Cargo.lock @@ -1488,7 +1488,7 @@ dependencies = [ [[package]] name = "fortuna" -version = "5.3.2" +version = "5.3.3" dependencies = [ "anyhow", "axum", diff --git a/apps/fortuna/Cargo.toml b/apps/fortuna/Cargo.toml index 264a0aafe1..015b17afe7 100644 --- a/apps/fortuna/Cargo.toml +++ b/apps/fortuna/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fortuna" -version = "5.3.2" +version = "5.3.3" edition = "2021" [dependencies] diff --git a/apps/fortuna/src/chain.rs b/apps/fortuna/src/chain.rs index 21680a6a0b..6570dc305d 100644 --- a/apps/fortuna/src/chain.rs +++ b/apps/fortuna/src/chain.rs @@ -1,2 +1,3 @@ +pub(crate) mod eth_gas_oracle; pub(crate) mod ethereum; pub(crate) mod reader; diff --git a/apps/fortuna/src/chain/eth_gas_oracle.rs b/apps/fortuna/src/chain/eth_gas_oracle.rs new file mode 100644 index 0000000000..ce9b6aa09c --- /dev/null +++ b/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 { + provider: M, +} + +impl EthProviderOracle { + 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 GasOracle for EthProviderOracle +where + M::Error: 'static, +{ + async fn fetch(&self) -> Result { + 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>) -> (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)| { + 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 + } +} diff --git a/apps/fortuna/src/chain/ethereum.rs b/apps/fortuna/src/chain/ethereum.rs index c890823f56..a7c030b008 100644 --- a/apps/fortuna/src/chain/ethereum.rs +++ b/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, LocalWallet>>, - LegacyTxTransformer, + GasOracleMiddleware< + TransformerMiddleware< + NonceManagerMiddleware, LocalWallet>>, + LegacyTxTransformer, + >, + EthProviderOracle>, >, >; pub type PythContract = PythRandom>; @@ -96,6 +103,8 @@ impl SignablePythContract { let provider = Provider::::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, )), )) } diff --git a/apps/fortuna/src/keeper.rs b/apps/fortuna/src/keeper.rs index 04899f4ee4..d40c0796b4 100644 --- a/apps/fortuna/src/keeper.rs +++ b/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::::new())); @@ -430,6 +430,7 @@ pub async fn process_event( .client() .inner() .inner() + .inner() .signer() .address() .to_string(),