Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fortuna): add eip1559_fee_multiplier_pct to adjust gas fees #2191

Closed
135 changes: 24 additions & 111 deletions apps/fortuna/src/chain/eth_gas_oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,35 @@ 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.
#[derive(Clone, Debug)]
#[must_use]
pub struct EthProviderOracle<M: Middleware> {
provider: M,
config: GasOracleConfig,
}

impl<M: Middleware> EthProviderOracle<M> {
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<u64>) -> Self {
Self {
provider,
config: GasOracleConfig {
eip1559_fee_multiplier_pct: eip1559_fee_multiplier_pct.unwrap_or(100),
},
}
}
}

Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should still be using eip1559_default_estimator. Please undo the deletion of that here.

.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<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]
}
.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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be adjusting max_priority_fee_per_gas, not max_fee_per_gas

Ok((adjusted_max_fee, max_priority_fee_per_gas))
}
}
5 changes: 4 additions & 1 deletion apps/fortuna/src/chain/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,10 @@ impl<T: JsonRpcClient + 'static + Clone> SignablePythContractInner<T> {
provider: Provider<T>,
) -> Result<SignablePythContractInner<T>> {
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::<LocalWallet>()?
.with_chain_id(chain_id.as_u64());
Expand Down
10 changes: 10 additions & 0 deletions apps/fortuna/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>,

/// 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.
Expand Down
27 changes: 19 additions & 8 deletions apps/fortuna/src/keeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use {
crate::{
api::{self, BlockchainState, ChainId},
chain::{
eth_gas_oracle::eip1559_default_estimator,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please revert all changes to this file -- it should be completely unchanged in the diff

ethereum::{
InstrumentedPythContract, InstrumentedSignablePythContract, PythContractCall,
},
Expand Down Expand Up @@ -277,6 +276,7 @@ pub async fn run_keeper_threads(
chain_eth_config.target_profit_pct,
chain_eth_config.max_profit_pct,
chain_eth_config.fee,
chain_eth_config.eip1559_fee_multiplier_pct,
)
.in_current_span(),
);
Expand Down Expand Up @@ -997,6 +997,7 @@ pub async fn adjust_fee_wrapper(
target_profit_pct: u64,
max_profit_pct: u64,
min_fee_wei: u128,
eip1559_fee_multiplier_pct: u64,
) {
// The maximum balance of accrued fees + provider wallet balance. None if we haven't observed a value yet.
let mut high_water_pnl: Option<U256> = None;
Expand All @@ -1014,6 +1015,7 @@ pub async fn adjust_fee_wrapper(
min_fee_wei,
&mut high_water_pnl,
&mut sequence_number_of_last_fee_update,
eip1559_fee_multiplier_pct,
)
.in_current_span()
.await
Expand Down Expand Up @@ -1097,6 +1099,7 @@ pub async fn adjust_fee_if_necessary(
min_fee_wei: u128,
high_water_pnl: &mut Option<U256>,
sequence_number_of_last_fee_update: &mut Option<u64>,
eip1559_fee_multiplier_pct: u64,
) -> Result<()> {
let provider_info = contract
.get_provider_info(provider_address)
Expand All @@ -1109,9 +1112,14 @@ pub async fn adjust_fee_if_necessary(
}

// Calculate target window for the on-chain fee.
let max_callback_cost: u128 = estimate_tx_cost(contract.clone(), legacy_tx, gas_limit.into())
.await
.map_err(|e| anyhow!("Could not estimate transaction cost. error {:?}", e))?;
let max_callback_cost: u128 = estimate_tx_cost(
contract.clone(),
legacy_tx,
gas_limit.into(),
eip1559_fee_multiplier_pct,
)
.await
.map_err(|e| anyhow!("Could not estimate transaction cost. error {:?}", e))?;
let target_fee_min = std::cmp::max(
(max_callback_cost * (100 + u128::from(min_profit_pct))) / 100,
min_fee_wei,
Expand Down Expand Up @@ -1197,6 +1205,7 @@ pub async fn estimate_tx_cost(
contract: Arc<InstrumentedSignablePythContract>,
use_legacy_tx: bool,
gas_used: u128,
eip1559_fee_multiplier_pct: u64,
) -> Result<u128> {
let middleware = contract.client();

Expand All @@ -1208,11 +1217,13 @@ 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?;

let total_fee = max_fee_per_gas + max_priority_fee_per_gas;
let adjusted_fee = total_fee * eip1559_fee_multiplier_pct / 100;

(max_fee_per_gas + max_priority_fee_per_gas)
adjusted_fee
.try_into()
.map_err(|e| anyhow!("gas price doesn't fit into 128 bits. error: {:?}", e))?
};
Expand Down
Loading