Skip to main content

anvil/eth/
fees.rs

1use std::{
2    collections::BTreeMap,
3    fmt,
4    pin::Pin,
5    sync::Arc,
6    task::{Context, Poll},
7};
8
9use alloy_consensus::{BlockHeader, Header, Transaction};
10use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams};
11use alloy_primitives::B256;
12use futures::StreamExt;
13use parking_lot::{Mutex, RwLock};
14use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
15
16use crate::eth::{
17    backend::{info::StorageInfo, notifications::NewBlockNotifications},
18    error::BlockchainError,
19};
20use foundry_primitives::FoundryNetwork;
21
22/// Maximum number of entries in the fee history cache
23pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
24
25/// Initial base fee for EIP-1559 blocks.
26pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
27
28/// Initial default gas price for the first block
29pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
30
31/// Bounds the amount the base fee can change between blocks.
32pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
33
34/// Minimum suggested priority fee
35pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
36
37/// Stores the fee related information
38#[derive(Clone, Debug)]
39pub struct FeeManager {
40    /// Hardfork identifier
41    spec_id: SpecId,
42    /// The blob params that determine blob fees
43    blob_params: Arc<RwLock<BlobParams>>,
44    /// Tracks the base fee for the next block post London
45    ///
46    /// This value will be updated after a new block was mined
47    base_fee: Arc<RwLock<u64>>,
48    /// Whether the minimum suggested priority fee is enforced
49    is_min_priority_fee_enforced: bool,
50    /// Tracks the excess blob gas, and the base fee, for the next block post Cancun
51    ///
52    /// This value will be updated after a new block was mined
53    blob_excess_gas_and_price: Arc<RwLock<BlobExcessGasAndPrice>>,
54    /// The base price to use Pre London
55    ///
56    /// This will be constant value unless changed manually
57    gas_price: Arc<RwLock<u128>>,
58    elasticity: Arc<RwLock<f64>>,
59    /// Network-specific base fee params for EIP-1559 calculations
60    base_fee_params: BaseFeeParams,
61}
62
63impl FeeManager {
64    pub fn new(
65        spec_id: SpecId,
66        base_fee: u64,
67        is_min_priority_fee_enforced: bool,
68        gas_price: u128,
69        blob_excess_gas_and_price: BlobExcessGasAndPrice,
70        blob_params: BlobParams,
71        base_fee_params: BaseFeeParams,
72    ) -> Self {
73        let elasticity = 1f64 / base_fee_params.elasticity_multiplier as f64;
74        Self {
75            spec_id,
76            blob_params: Arc::new(RwLock::new(blob_params)),
77            base_fee: Arc::new(RwLock::new(base_fee)),
78            is_min_priority_fee_enforced,
79            gas_price: Arc::new(RwLock::new(gas_price)),
80            blob_excess_gas_and_price: Arc::new(RwLock::new(blob_excess_gas_and_price)),
81            elasticity: Arc::new(RwLock::new(elasticity)),
82            base_fee_params,
83        }
84    }
85
86    /// Returns the base fee params used for EIP-1559 calculations
87    pub fn base_fee_params(&self) -> BaseFeeParams {
88        self.base_fee_params
89    }
90
91    pub fn elasticity(&self) -> f64 {
92        *self.elasticity.read()
93    }
94
95    /// Returns true for post London
96    pub fn is_eip1559(&self) -> bool {
97        (self.spec_id as u8) >= (SpecId::LONDON as u8)
98    }
99
100    pub fn is_eip4844(&self) -> bool {
101        (self.spec_id as u8) >= (SpecId::CANCUN as u8)
102    }
103
104    /// Calculates the current blob gas price
105    pub fn blob_gas_price(&self) -> u128 {
106        if self.is_eip4844() { self.base_fee_per_blob_gas() } else { 0 }
107    }
108
109    pub fn base_fee(&self) -> u64 {
110        if self.is_eip1559() { *self.base_fee.read() } else { 0 }
111    }
112
113    pub fn is_min_priority_fee_enforced(&self) -> bool {
114        self.is_min_priority_fee_enforced
115    }
116
117    /// Raw base gas price
118    pub fn raw_gas_price(&self) -> u128 {
119        *self.gas_price.read()
120    }
121
122    pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
123        if self.is_eip4844() { Some(*self.blob_excess_gas_and_price.read()) } else { None }
124    }
125
126    pub fn base_fee_per_blob_gas(&self) -> u128 {
127        if self.is_eip4844() { self.blob_excess_gas_and_price.read().blob_gasprice } else { 0 }
128    }
129
130    /// Returns the current gas price
131    pub fn set_gas_price(&self, price: u128) {
132        let mut gas = self.gas_price.write();
133        *gas = price;
134    }
135
136    /// Returns the current base fee
137    pub fn set_base_fee(&self, fee: u64) {
138        trace!(target: "backend::fees", "updated base fee {:?}", fee);
139        let mut base = self.base_fee.write();
140        *base = fee;
141    }
142
143    /// Sets the current blob excess gas and price
144    pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
145        trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
146        let mut base = self.blob_excess_gas_and_price.write();
147        *base = blob_excess_gas_and_price;
148    }
149
150    /// Calculates the base fee for the next block
151    pub fn get_next_block_base_fee_per_gas(
152        &self,
153        gas_used: u64,
154        gas_limit: u64,
155        last_fee_per_gas: u64,
156    ) -> u64 {
157        // It's naturally impossible for base fee to be 0;
158        // It means it was set by the user deliberately and therefore we treat it as a constant.
159        // Therefore, we skip the base fee calculation altogether and we return 0.
160        if self.base_fee() == 0 {
161            return 0;
162        }
163        calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, self.base_fee_params)
164    }
165
166    /// Calculates the next block blob base fee.
167    pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
168        self.blob_params().calc_blob_fee(self.blob_excess_gas_and_price.read().excess_blob_gas)
169    }
170
171    /// Calculates the next block blob excess gas, using the provided parent blob excess gas and
172    /// parent blob gas used
173    pub fn get_next_block_blob_excess_gas(&self, blob_excess_gas: u64, blob_gas_used: u64) -> u64 {
174        self.blob_params().next_block_excess_blob_gas_osaka(
175            blob_excess_gas,
176            blob_gas_used,
177            self.base_fee(),
178        )
179    }
180
181    /// Configures the blob params
182    pub fn set_blob_params(&self, blob_params: BlobParams) {
183        *self.blob_params.write() = blob_params;
184    }
185
186    /// Returns the active [`BlobParams`]
187    pub fn blob_params(&self) -> BlobParams {
188        *self.blob_params.read()
189    }
190}
191
192/// An async service that takes care of the `FeeHistory` cache
193pub struct FeeHistoryService {
194    /// blob parameters for the current spec
195    blob_params: BlobParams,
196    /// incoming notifications about new blocks
197    new_blocks: NewBlockNotifications,
198    /// contains all fee history related entries
199    cache: FeeHistoryCache,
200    /// number of items to consider
201    fee_history_limit: u64,
202    /// a type that can fetch ethereum-storage data
203    storage_info: StorageInfo<FoundryNetwork>,
204}
205
206impl FeeHistoryService {
207    pub fn new(
208        blob_params: BlobParams,
209        new_blocks: NewBlockNotifications,
210        cache: FeeHistoryCache,
211        storage_info: StorageInfo<FoundryNetwork>,
212    ) -> Self {
213        Self {
214            blob_params,
215            new_blocks,
216            cache,
217            fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
218            storage_info,
219        }
220    }
221
222    /// Returns the configured history limit
223    pub fn fee_history_limit(&self) -> u64 {
224        self.fee_history_limit
225    }
226
227    /// Inserts a new cache entry for the given block
228    pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &Header) {
229        let (result, block_number) = self.create_cache_entry(hash, header);
230        self.insert_cache_entry(result, block_number);
231    }
232
233    /// Create a new history entry for the block
234    fn create_cache_entry(
235        &self,
236        hash: B256,
237        header: &Header,
238    ) -> (FeeHistoryCacheItem, Option<u64>) {
239        // percentile list from 0.0 to 100.0 with a 0.5 resolution.
240        // this will create 200 percentile points
241        let reward_percentiles: Vec<f64> = {
242            let mut percentile: f64 = 0.0;
243            (0..=200)
244                .map(|_| {
245                    let val = percentile;
246                    percentile += 0.5;
247                    val
248                })
249                .collect()
250        };
251
252        let mut block_number: Option<u64> = None;
253        let base_fee = header.base_fee_per_gas.unwrap_or_default();
254        let excess_blob_gas = header.excess_blob_gas.map(|g| g as u128);
255        let blob_gas_used = header.blob_gas_used.map(|g| g as u128);
256        let base_fee_per_blob_gas = header.blob_fee(self.blob_params);
257
258        let mut item = FeeHistoryCacheItem {
259            base_fee: base_fee as u128,
260            gas_used_ratio: 0f64,
261            blob_gas_used_ratio: 0f64,
262            rewards: Vec::new(),
263            excess_blob_gas,
264            base_fee_per_blob_gas,
265            blob_gas_used,
266        };
267
268        let current_block = self.storage_info.block(hash);
269        let current_receipts = self.storage_info.receipts(hash);
270
271        if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
272            block_number = Some(block.header.number());
273
274            let gas_used = block.header.gas_used() as f64;
275            let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
276            item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
277            item.blob_gas_used_ratio = blob_gas_used
278                .map(|g| {
279                    let max = self.blob_params.max_blob_gas_per_block() as f64;
280                    if max == 0.0 { 0.0 } else { g / max }
281                })
282                .unwrap_or(0.0);
283
284            // extract useful tx info (gas_used, effective_reward)
285            let mut transactions: Vec<(_, _)> = receipts
286                .iter()
287                .enumerate()
288                .map(|(i, receipt)| {
289                    let cumulative = receipt.cumulative_gas_used();
290                    let prev_cumulative =
291                        if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
292                    let gas_used = cumulative - prev_cumulative;
293                    let effective_reward = block
294                        .body
295                        .transactions
296                        .get(i)
297                        .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
298                        .unwrap_or(0);
299
300                    (gas_used, effective_reward)
301                })
302                .collect();
303
304            // sort by effective reward asc
305            transactions.sort_by_key(|(_, reward)| *reward);
306
307            // calculate percentile rewards
308            item.rewards = reward_percentiles
309                .into_iter()
310                .filter_map(|p| {
311                    let target_gas = (p * gas_used / 100f64) as u64;
312                    let mut sum_gas = 0;
313                    for (gas_used, effective_reward) in transactions.iter().copied() {
314                        sum_gas += gas_used;
315                        if target_gas <= sum_gas {
316                            return Some(effective_reward);
317                        }
318                    }
319                    None
320                })
321                .collect();
322        } else {
323            item.rewards = vec![0; reward_percentiles.len()];
324        }
325        (item, block_number)
326    }
327
328    fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
329        if let Some(block_number) = block_number {
330            trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
331            let mut cache = self.cache.lock();
332            cache.insert(block_number, item);
333
334            // adhere to cache limit
335            let pop_next = block_number.saturating_sub(self.fee_history_limit);
336
337            let num_remove = (cache.len() as u64).saturating_sub(self.fee_history_limit);
338            for num in 0..num_remove {
339                let key = pop_next - num;
340                cache.remove(&key);
341            }
342        }
343    }
344}
345
346// An endless future that listens for new blocks and updates the cache
347impl Future for FeeHistoryService {
348    type Output = ();
349
350    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
351        let pin = self.get_mut();
352
353        while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
354            // add the imported block.
355            pin.insert_cache_entry_for_block(notification.hash, notification.header.as_ref());
356        }
357
358        Poll::Pending
359    }
360}
361
362pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
363
364/// A single item in the whole fee history cache
365#[derive(Clone, Debug)]
366pub struct FeeHistoryCacheItem {
367    pub base_fee: u128,
368    pub gas_used_ratio: f64,
369    pub base_fee_per_blob_gas: Option<u128>,
370    pub blob_gas_used_ratio: f64,
371    pub excess_blob_gas: Option<u128>,
372    pub blob_gas_used: Option<u128>,
373    pub rewards: Vec<u128>,
374}
375
376#[derive(Clone, Default)]
377pub struct FeeDetails {
378    pub gas_price: Option<u128>,
379    pub max_fee_per_gas: Option<u128>,
380    pub max_priority_fee_per_gas: Option<u128>,
381    pub max_fee_per_blob_gas: Option<u128>,
382}
383
384impl FeeDetails {
385    /// All values zero
386    pub fn zero() -> Self {
387        Self {
388            gas_price: Some(0),
389            max_fee_per_gas: Some(0),
390            max_priority_fee_per_gas: Some(0),
391            max_fee_per_blob_gas: None,
392        }
393    }
394
395    /// If neither `gas_price` nor `max_fee_per_gas` is `Some`, this will set both to `0`
396    pub fn or_zero_fees(self) -> Self {
397        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
398            self;
399
400        let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
401        let gas_price = if no_fees { Some(0) } else { gas_price };
402        let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
403        let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
404
405        Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
406    }
407
408    /// Turns this type into a tuple
409    pub fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
410        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
411            self;
412        (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
413    }
414
415    /// Creates a new instance from the request's gas related values
416    pub fn new(
417        request_gas_price: Option<u128>,
418        request_max_fee: Option<u128>,
419        request_priority: Option<u128>,
420        max_fee_per_blob_gas: Option<u128>,
421    ) -> Result<Self, BlockchainError> {
422        match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
423            (gas_price, None, None, None) => {
424                // Legacy request, all default to gas price.
425                Ok(Self {
426                    gas_price,
427                    max_fee_per_gas: gas_price,
428                    max_priority_fee_per_gas: gas_price,
429                    max_fee_per_blob_gas: None,
430                })
431            }
432            (_, max_fee, max_priority, None) => {
433                // eip-1559
434                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
435                if let Some(max_priority) = max_priority {
436                    let max_fee = max_fee.unwrap_or_default();
437                    if max_priority > max_fee {
438                        return Err(BlockchainError::InvalidFeeInput);
439                    }
440                }
441                Ok(Self {
442                    gas_price: max_fee,
443                    max_fee_per_gas: max_fee,
444                    max_priority_fee_per_gas: max_priority,
445                    max_fee_per_blob_gas: None,
446                })
447            }
448            (_, max_fee, max_priority, max_fee_per_blob_gas) => {
449                // eip-1559
450                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
451                if let Some(max_priority) = max_priority {
452                    let max_fee = max_fee.unwrap_or_default();
453                    if max_priority > max_fee {
454                        return Err(BlockchainError::InvalidFeeInput);
455                    }
456                }
457                Ok(Self {
458                    gas_price: max_fee,
459                    max_fee_per_gas: max_fee,
460                    max_priority_fee_per_gas: max_priority,
461                    max_fee_per_blob_gas,
462                })
463            }
464        }
465    }
466}
467
468impl fmt::Debug for FeeDetails {
469    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
470        write!(fmt, "Fees {{ ")?;
471        write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
472        write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
473        write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
474        write!(fmt, "}}")?;
475        Ok(())
476    }
477}