anvil/eth/
fees.rs

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