1use alloy_consensus::BlockHeader;
8use alloy_eips::BlockNumberOrTag;
9use alloy_network::{BlockResponse, Network};
10use alloy_provider::{Provider, utils::Eip1559Estimation};
11use eyre::{Result, WrapErr};
12use foundry_config::Eip1559FeeEstimatePreset;
13
14const FEE_HISTORY_BLOCKS: u64 = 10;
16
17const MIN_PRIORITY_FEE: u128 = 1;
19
20const MIN_GAS_USED_RATIO: f64 = 0.1;
25
26#[derive(Clone, Copy, Debug)]
31pub struct ResolvedEip1559Fees {
32 pub max_fee_per_gas: u128,
34 pub max_priority_fee_per_gas: u128,
36 pub base_fee_per_gas: u128,
38}
39
40impl ResolvedEip1559Fees {
41 pub const fn estimation(&self) -> Eip1559Estimation {
44 Eip1559Estimation {
45 max_fee_per_gas: self.max_fee_per_gas,
46 max_priority_fee_per_gas: self.max_priority_fee_per_gas,
47 }
48 }
49}
50
51pub async fn estimate_eip1559_fees<P, N>(
56 provider: &P,
57 preset: Eip1559FeeEstimatePreset,
58) -> Result<ResolvedEip1559Fees>
59where
60 P: Provider<N>,
61 N: Network,
62{
63 let fee_history = provider
64 .get_fee_history(
65 FEE_HISTORY_BLOCKS,
66 BlockNumberOrTag::Latest,
67 &[preset.reward_percentile()],
68 )
69 .await
70 .wrap_err("Failed to fetch fee history for EIP-1559 estimation")?;
71
72 let base_fee_per_gas = match fee_history.latest_block_base_fee() {
76 Some(base_fee) if base_fee != 0 => base_fee,
77 _ => provider
78 .get_block_by_number(BlockNumberOrTag::Latest)
79 .await
80 .wrap_err("Failed to fetch latest block for EIP-1559 base fee")?
81 .ok_or_else(|| eyre::eyre!("Latest block not found"))?
82 .header()
83 .as_ref()
84 .base_fee_per_gas()
85 .ok_or_else(|| {
86 eyre::eyre!(
87 "Chain does not appear to support EIP-1559; try adding --legacy to your command."
88 )
89 })?
90 .into(),
91 };
92
93 let max_priority_fee_per_gas = estimate_priority_fee(
94 fee_history.reward.as_deref().unwrap_or_default(),
95 &fee_history.gas_used_ratio,
96 );
97
98 let (num, den) = preset.base_fee_multiplier();
99 let max_fee_per_gas = base_fee_per_gas
100 .checked_mul(num)
101 .map_or(u128::MAX, |scaled| scaled / den)
102 .saturating_add(max_priority_fee_per_gas);
103
104 Ok(ResolvedEip1559Fees { max_fee_per_gas, max_priority_fee_per_gas, base_fee_per_gas })
105}
106
107pub fn resolve_broadcast_eip1559_fees(
114 mut fees: ResolvedEip1559Fees,
115 with_gas_price: Option<u128>,
116 priority_gas_price: Option<u128>,
117 browser_suggested_tip: Option<u128>,
118) -> Result<ResolvedEip1559Fees> {
119 if let Some(suggested_tip) = browser_suggested_tip
122 && suggested_tip > fees.max_priority_fee_per_gas
123 {
124 let delta = suggested_tip - fees.max_priority_fee_per_gas;
125 fees.max_fee_per_gas = fees.max_fee_per_gas.saturating_add(delta);
126 fees.max_priority_fee_per_gas = suggested_tip;
127 }
128
129 if let Some(max_fee_per_gas) = with_gas_price {
130 fees.max_fee_per_gas = max_fee_per_gas;
131 }
132
133 if let Some(max_priority_fee_per_gas) = priority_gas_price {
134 fees.max_priority_fee_per_gas = max_priority_fee_per_gas;
135 }
136
137 if fees.max_priority_fee_per_gas > fees.max_fee_per_gas {
138 eyre::bail!(
139 "maxPriorityFeePerGas ({}) cannot be higher than maxFeePerGas ({})",
140 fees.max_priority_fee_per_gas,
141 fees.max_fee_per_gas,
142 );
143 }
144
145 Ok(fees)
146}
147
148fn estimate_priority_fee(rewards: &[Vec<u128>], gas_used_ratio: &[f64]) -> u128 {
158 let apply_filter = gas_used_ratio.len() == rewards.len();
159 let non_zero_rewards = |filtered: bool| {
160 rewards
161 .iter()
162 .enumerate()
163 .filter(|(i, _)| {
164 !filtered
165 || (gas_used_ratio[*i].is_finite() && gas_used_ratio[*i] >= MIN_GAS_USED_RATIO)
166 })
167 .filter_map(|(_, reward)| reward.first().copied())
168 .filter(|reward| *reward > 0)
169 .collect::<Vec<_>>()
170 };
171
172 let mut rewards = non_zero_rewards(apply_filter);
173 if apply_filter && rewards.is_empty() {
177 let unfiltered = non_zero_rewards(false);
178 if unfiltered.len() * 2 >= gas_used_ratio.len() {
179 rewards = unfiltered;
180 }
181 }
182 if rewards.is_empty() {
183 return MIN_PRIORITY_FEE;
184 }
185
186 rewards.sort_unstable();
187
188 let n = rewards.len();
189 let median =
191 if n % 2 == 0 { rewards[n / 2 - 1].midpoint(rewards[n / 2]) } else { rewards[n / 2] };
192
193 std::cmp::max(median, MIN_PRIORITY_FEE)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 const BUSY: f64 = 0.5;
202
203 #[test]
204 fn priority_fee_median_of_busy_blocks() {
205 assert_eq!(estimate_priority_fee(&[], &[]), MIN_PRIORITY_FEE);
207 assert_eq!(estimate_priority_fee(&[vec![0], vec![0]], &[BUSY, BUSY]), MIN_PRIORITY_FEE);
208
209 assert_eq!(estimate_priority_fee(&[vec![1], vec![3], vec![5]], &[BUSY, BUSY, BUSY]), 3);
211 assert_eq!(estimate_priority_fee(&[vec![2], vec![4]], &[BUSY, BUSY]), 3);
212 }
213
214 #[test]
215 fn priority_fee_ignores_near_empty_blocks() {
216 let rewards = vec![vec![0u128], vec![41_000_000_000_000u128], vec![0u128]];
218 let ratios = vec![0.0, 0.001, 0.0];
219 assert_eq!(estimate_priority_fee(&rewards, &ratios), MIN_PRIORITY_FEE);
220
221 let rewards = vec![vec![500u128], vec![2u128], vec![800u128], vec![3u128], vec![999u128]];
223 let ratios = vec![0.01, BUSY, 0.03, 0.7, 0.02];
224 assert_eq!(estimate_priority_fee(&rewards, &ratios), 2);
225
226 assert_eq!(estimate_priority_fee(&[vec![10], vec![20]], &[0.1, 0.09]), 10);
228
229 assert_eq!(estimate_priority_fee(&[vec![5], vec![6]], &[f64::NAN, f64::INFINITY]), 5);
231 }
232
233 #[test]
234 fn priority_fee_falls_back_when_tipping_is_the_norm() {
235 let rewards = vec![vec![1_000u128]; 10];
237 let ratios = vec![0.05; 10];
238 assert_eq!(estimate_priority_fee(&rewards, &ratios), 1_000);
239
240 let rewards = vec![vec![0u128], vec![10u128], vec![0u128], vec![20u128]];
242 let ratios = vec![0.01; 4];
243 assert_eq!(estimate_priority_fee(&rewards, &ratios), 15);
244
245 let mut rewards = vec![vec![0u128]; 10];
247 rewards[3] = vec![41_000_000_000_000u128];
248 let ratios = vec![0.01; 10];
249 assert_eq!(estimate_priority_fee(&rewards, &ratios), MIN_PRIORITY_FEE);
250 }
251
252 #[test]
253 fn priority_fee_skips_filter_on_length_mismatch() {
254 assert_eq!(estimate_priority_fee(&[vec![7], vec![9], vec![11]], &[BUSY]), 9);
257 assert_eq!(estimate_priority_fee(&[vec![7]], &[BUSY, BUSY, BUSY]), 7);
258 assert_eq!(estimate_priority_fee(&[vec![2], vec![4]], &[]), 3);
259 }
260
261 #[tokio::test]
263 async fn estimate_filters_near_empty_block_outliers() {
264 use alloy_provider::{ProviderBuilder, mock::Asserter};
265 use alloy_rpc_types::FeeHistory;
266
267 let base = 20_000_000_000u128; let big = 41_000_000_000_000u128; let fee_history = FeeHistory {
270 base_fee_per_gas: vec![base; 11],
271 gas_used_ratio: vec![0.001; 10],
272 base_fee_per_blob_gas: vec![1; 11],
273 blob_gas_used_ratio: vec![0.0; 10],
274 oldest_block: 1,
275 reward: Some(vec![
276 vec![0u128],
277 vec![big],
278 vec![0u128],
279 vec![big],
280 vec![0u128],
281 vec![big],
282 vec![0u128],
283 vec![0u128],
284 vec![0u128],
285 vec![0u128],
286 ]),
287 };
288
289 let asserter = Asserter::new();
290 asserter.push_success(&fee_history);
291 let provider = ProviderBuilder::new_with_network::<alloy_network::Ethereum>()
292 .connect_mocked_client(asserter);
293
294 let fees =
295 estimate_eip1559_fees(&provider, Eip1559FeeEstimatePreset::Market).await.unwrap();
296
297 assert_eq!(fees.base_fee_per_gas, base);
299 assert_eq!(fees.max_priority_fee_per_gas, MIN_PRIORITY_FEE);
300 assert_eq!(fees.max_fee_per_gas, 40_000_000_001);
301 }
302
303 fn fees(max: u128, priority: u128) -> ResolvedEip1559Fees {
304 ResolvedEip1559Fees {
305 max_fee_per_gas: max,
306 max_priority_fee_per_gas: priority,
307 base_fee_per_gas: 100,
308 }
309 }
310
311 #[test]
312 fn resolve_overrides_each_field_independently() {
313 let r = resolve_broadcast_eip1559_fees(fees(300, 50), None, None, None).unwrap();
315 assert_eq!((r.max_fee_per_gas, r.max_priority_fee_per_gas), (300, 50));
316
317 let r = resolve_broadcast_eip1559_fees(fees(300, 50), Some(500), None, None).unwrap();
319 assert_eq!((r.max_fee_per_gas, r.max_priority_fee_per_gas), (500, 50));
320 let r = resolve_broadcast_eip1559_fees(fees(300, 50), None, Some(80), None).unwrap();
321 assert_eq!((r.max_fee_per_gas, r.max_priority_fee_per_gas), (300, 80));
322
323 let err = resolve_broadcast_eip1559_fees(fees(300, 50), None, Some(400), None).unwrap_err();
325 assert!(err.to_string().contains("cannot be higher than maxFeePerGas"));
326 }
327
328 #[test]
329 fn resolve_browser_tip_raises_both_caps_by_delta() {
330 let r = resolve_broadcast_eip1559_fees(fees(300, 50), None, None, Some(120)).unwrap();
332 assert_eq!((r.max_fee_per_gas, r.max_priority_fee_per_gas), (370, 120)); let r = resolve_broadcast_eip1559_fees(fees(300, 50), None, None, Some(10)).unwrap();
336 assert_eq!((r.max_fee_per_gas, r.max_priority_fee_per_gas), (300, 50));
337 let r = resolve_broadcast_eip1559_fees(fees(u128::MAX, 50), None, None, Some(120)).unwrap();
338 assert_eq!((r.max_fee_per_gas, r.max_priority_fee_per_gas), (u128::MAX, 120));
339 }
340
341 #[test]
342 fn market_preset_max_fee_formula() {
343 let preset = Eip1559FeeEstimatePreset::Market;
345 let (num, den) = preset.base_fee_multiplier();
346 let base_fee = 2_000_000_000u128; let priority = estimate_priority_fee(&[vec![100], vec![300]], &[BUSY, BUSY]);
348 let max_fee = base_fee.saturating_mul(num) / den + priority;
349 assert_eq!(max_fee, base_fee * 2 + priority);
350 }
351}