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