anvil/eth/
fees.rs

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