diff --git a/apps/fortuna/src/chain/eth_gas_oracle.rs b/apps/fortuna/src/chain/eth_gas_oracle.rs index 869c00eab7..3a7106af2e 100644 --- a/apps/fortuna/src/chain/eth_gas_oracle.rs +++ b/apps/fortuna/src/chain/eth_gas_oracle.rs @@ -6,32 +6,15 @@ use { GasOracle, }, providers::Middleware, - types::{I256, U256}, + types::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; +/// Configuration for GasOracle +#[derive(Clone, Debug)] +pub struct GasOracleConfig { + pub eip1559_fee_multiplier_pct: u64, +} /// Gas oracle from a [`Middleware`] implementation such as an /// Ethereum RPC provider. @@ -39,11 +22,19 @@ pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200; #[must_use] pub struct EthProviderOracle { provider: M, + config: GasOracleConfig, } impl EthProviderOracle { - pub fn new(provider: M) -> Self { - Self { provider } + /// Creates a new EthProviderOracle with the given provider and optional fee multiplier. + /// If no multiplier is provided, defaults to 100% (no change to fees). + pub fn new(provider: M, eip1559_fee_multiplier_pct: Option) -> Self { + Self { + provider, + config: GasOracleConfig { + eip1559_fee_multiplier_pct: eip1559_fee_multiplier_pct.unwrap_or(100), + }, + } } } @@ -61,93 +52,15 @@ where } async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> { - self.provider - .estimate_eip1559_fees(Some(eip1559_default_estimator)) + let (max_fee_per_gas, max_priority_fee_per_gas) = self + .provider + .estimate_eip1559_fees(None) .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) -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)| { - 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] -} + .map_err(|err| GasOracleError::ProviderError(Box::new(err)))?; -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 + // Apply the fee multiplier + let multiplier = U256::from(self.config.eip1559_fee_multiplier_pct); + let adjusted_max_fee = (max_fee_per_gas * multiplier) / U256::from(100); + Ok((adjusted_max_fee, max_priority_fee_per_gas)) } } diff --git a/apps/fortuna/src/chain/ethereum.rs b/apps/fortuna/src/chain/ethereum.rs index f4f97a07df..a1ab26e86b 100644 --- a/apps/fortuna/src/chain/ethereum.rs +++ b/apps/fortuna/src/chain/ethereum.rs @@ -211,7 +211,10 @@ impl SignablePythContractInner { provider: Provider, ) -> Result> { let chain_id = provider.get_chainid().await?; - let gas_oracle = EthProviderOracle::new(provider.clone()); + let gas_oracle = EthProviderOracle::new( + provider.clone(), + Some(chain_config.eip1559_fee_multiplier_pct), + ); let wallet__ = private_key .parse::()? .with_chain_id(chain_id.as_u64()); diff --git a/apps/fortuna/src/config.rs b/apps/fortuna/src/config.rs index 87707aee20..fd17b65111 100644 --- a/apps/fortuna/src/config.rs +++ b/apps/fortuna/src/config.rs @@ -166,6 +166,16 @@ pub struct EthereumConfig { /// Maximum number of hashes to record in a request. /// This should be set according to the maximum gas limit the provider supports for callbacks. pub max_num_hashes: Option, + + /// Multiplier for EIP1559 fee estimates, represented as a percentage. + /// For example, 100 means no change, 200 means double the fees. + #[serde(default = "default_eip1559_fee_multiplier_pct")] + pub eip1559_fee_multiplier_pct: u64, +} + +/// Default value for eip1559_fee_multiplier_pct (100 = no change to fees) +fn default_eip1559_fee_multiplier_pct() -> u64 { + 100 } /// A commitment that the provider used to generate random numbers at some point in the past. diff --git a/apps/fortuna/src/keeper.rs b/apps/fortuna/src/keeper.rs index b091d94fa8..d1a3b1584e 100644 --- a/apps/fortuna/src/keeper.rs +++ b/apps/fortuna/src/keeper.rs @@ -2,7 +2,6 @@ use { crate::{ api::{self, BlockchainState, ChainId}, chain::{ - eth_gas_oracle::eip1559_default_estimator, ethereum::{ InstrumentedPythContract, InstrumentedSignablePythContract, PythContractCall, }, @@ -215,121 +214,100 @@ pub async fn run_keeper_threads( // Spawn a thread to handle the events from last BACKLOG_RANGE blocks. let gas_limit: U256 = chain_eth_config.gas_limit.into(); - spawn( - process_backlog( - BlockRange { - from: latest_safe_block.saturating_sub(BACKLOG_RANGE), - to: latest_safe_block, - }, - contract.clone(), - gas_limit, - chain_state.clone(), - metrics.clone(), - fulfilled_requests_cache.clone(), - ) - .in_current_span(), - ); + spawn(process_backlog( + BlockRange { + from: latest_safe_block.saturating_sub(BACKLOG_RANGE), + to: latest_safe_block, + }, + Arc::clone(&contract), + gas_limit, + chain_state.clone(), + metrics.clone(), + fulfilled_requests_cache.clone(), + )); let (tx, rx) = mpsc::channel::(1000); // Spawn a thread to watch for new blocks and send the range of blocks for which events has not been handled to the `tx` channel. - spawn( - watch_blocks_wrapper( - chain_state.clone(), - latest_safe_block, - tx, - chain_eth_config.geth_rpc_wss.clone(), - ) - .in_current_span(), - ); + spawn(watch_blocks_wrapper( + chain_state.clone(), + latest_safe_block, + tx, + chain_eth_config.geth_rpc_wss.clone(), + )); // Spawn a thread that listens for block ranges on the `rx` channel and processes the events for those blocks. - spawn( - process_new_blocks( - chain_state.clone(), - rx, - Arc::clone(&contract), - gas_limit, - metrics.clone(), - fulfilled_requests_cache.clone(), - ) - .in_current_span(), - ); + spawn(process_new_blocks( + chain_state.clone(), + rx, + Arc::clone(&contract), + gas_limit, + metrics.clone(), + fulfilled_requests_cache.clone(), + )); // Spawn a thread that watches the keeper wallet balance and submits withdrawal transactions as needed to top-up the balance. - spawn( - withdraw_fees_wrapper( - contract.clone(), - chain_state.provider_address, - WITHDRAW_INTERVAL, - U256::from(chain_eth_config.min_keeper_balance), - ) - .in_current_span(), - ); + spawn(withdraw_fees_wrapper( + Arc::clone(&contract), + chain_state.provider_address, + WITHDRAW_INTERVAL, + U256::from(chain_eth_config.min_keeper_balance), + )); // Spawn a thread that periodically adjusts the provider fee. - spawn( - adjust_fee_wrapper( - contract.clone(), - chain_state.provider_address, - ADJUST_FEE_INTERVAL, - chain_eth_config.legacy_tx, - chain_eth_config.gas_limit, - chain_eth_config.min_profit_pct, - chain_eth_config.target_profit_pct, - chain_eth_config.max_profit_pct, - chain_eth_config.fee, - ) - .in_current_span(), - ); + spawn(adjust_fee_wrapper( + Arc::clone(&contract), + chain_state.provider_address, + ADJUST_FEE_INTERVAL, + chain_eth_config.legacy_tx, + chain_eth_config.gas_limit, + chain_eth_config.min_profit_pct, + chain_eth_config.target_profit_pct, + chain_eth_config.max_profit_pct, + chain_eth_config.fee, + )); - spawn(update_commitments_loop(contract.clone(), chain_state.clone()).in_current_span()); + spawn(update_commitments_loop( + Arc::clone(&contract), + chain_state.clone(), + )); // Spawn a thread to track the provider info and the balance of the keeper - spawn( - async move { - let chain_id = chain_state.id.clone(); - let chain_config = chain_eth_config.clone(); - let provider_address = chain_state.provider_address; - let keeper_metrics = metrics.clone(); - let contract = match InstrumentedPythContract::from_config( - &chain_config, - chain_id.clone(), - rpc_metrics, - ) { - Ok(r) => r, - Err(e) => { - tracing::error!("Error while connecting to pythnet contract. error: {:?}", e); - return; - } - }; + spawn(async move { + let chain_id = chain_state.id.clone(); + let chain_config = chain_eth_config.clone(); + let provider_address = chain_state.provider_address; + let keeper_metrics = metrics.clone(); + let contract = match InstrumentedPythContract::from_config( + &chain_config, + chain_id.clone(), + rpc_metrics, + ) { + Ok(r) => r, + Err(e) => { + tracing::error!("Error while connecting to pythnet contract. error: {:?}", e); + return; + } + }; - loop { - // There isn't a loop for indefinite trials. There is a new thread being spawned every `TRACK_INTERVAL` seconds. - // If rpc start fails all of these threads will just exit, instead of retrying. - // We are tracking rpc failures elsewhere, so it's fine. - spawn( - track_provider( - chain_id.clone(), - contract.clone(), - provider_address, - keeper_metrics.clone(), - ) - .in_current_span(), - ); - spawn( - track_balance( - chain_id.clone(), - contract.client(), - keeper_address, - keeper_metrics.clone(), - ) - .in_current_span(), - ); + loop { + // There isn't a loop for indefinite trials. There is a new thread being spawned every `TRACK_INTERVAL` seconds. + // If rpc start fails all of these threads will just exit, instead of retrying. + // We are tracking rpc failures elsewhere, so it's fine. + spawn(track_provider( + chain_id.clone(), + contract.clone(), + provider_address, + keeper_metrics.clone(), + )); + spawn(track_balance( + chain_id.clone(), + contract.client(), + keeper_address, + keeper_metrics.clone(), + )); - time::sleep(TRACK_INTERVAL).await; - } + time::sleep(TRACK_INTERVAL).await; } - .in_current_span(), - ); + }); } /// Process an event with backoff. It will retry the reveal on failure for 5 minutes. @@ -1004,7 +982,7 @@ pub async fn adjust_fee_wrapper( let mut sequence_number_of_last_fee_update: Option = None; loop { if let Err(e) = adjust_fee_if_necessary( - contract.clone(), + Arc::clone(&contract), provider_address, legacy_tx, gas_limit, @@ -1015,10 +993,9 @@ pub async fn adjust_fee_wrapper( &mut high_water_pnl, &mut sequence_number_of_last_fee_update, ) - .in_current_span() .await { - tracing::error!("Withdrawing fees. error: {:?}", e); + tracing::error!("Error adjusting fees. error: {:?}", e); } time::sleep(poll_interval).await; } @@ -1030,10 +1007,7 @@ pub async fn update_commitments_loop( chain_state: BlockchainState, ) { loop { - if let Err(e) = update_commitments_if_necessary(contract.clone(), &chain_state) - .in_current_span() - .await - { + if let Err(e) = update_commitments_if_necessary(Arc::clone(&contract), &chain_state).await { tracing::error!("Update commitments. error: {:?}", e); } time::sleep(UPDATE_COMMITMENTS_INTERVAL).await; @@ -1208,9 +1182,8 @@ pub async fn estimate_tx_cost( .try_into() .map_err(|e| anyhow!("gas price doesn't fit into 128 bits. error: {:?}", e))? } else { - let (max_fee_per_gas, max_priority_fee_per_gas) = middleware - .estimate_eip1559_fees(Some(eip1559_default_estimator)) - .await?; + let (max_fee_per_gas, max_priority_fee_per_gas) = + middleware.estimate_eip1559_fees(None).await?; (max_fee_per_gas + max_priority_fee_per_gas) .try_into()