reth_rpc_convert/
fees.rs

1use alloy_primitives::{B256, U256};
2use std::cmp::min;
3use thiserror::Error;
4
5/// Helper type for representing the fees of a `TransactionRequest`
6#[derive(Debug)]
7pub struct CallFees {
8    /// EIP-1559 priority fee
9    pub max_priority_fee_per_gas: Option<U256>,
10    /// Unified gas price setting
11    ///
12    /// Will be the configured `basefee` if unset in the request
13    ///
14    /// `gasPrice` for legacy,
15    /// `maxFeePerGas` for EIP-1559
16    pub gas_price: U256,
17    /// Max Fee per Blob gas for EIP-4844 transactions
18    pub max_fee_per_blob_gas: Option<U256>,
19}
20
21impl CallFees {
22    /// Ensures the fields of a `TransactionRequest` are not conflicting.
23    ///
24    /// # EIP-4844 transactions
25    ///
26    /// Blob transactions have an additional fee parameter `maxFeePerBlobGas`.
27    /// If the `maxFeePerBlobGas` or `blobVersionedHashes` are set we treat it as an EIP-4844
28    /// transaction.
29    ///
30    /// Note: Due to the `Default` impl of [`BlockEnv`] (Some(0)) this assumes the `block_blob_fee`
31    /// is always `Some`
32    ///
33    /// ## Notable design decisions
34    ///
35    /// For compatibility reasons, this contains several exceptions when fee values are validated:
36    /// - If both `maxFeePerGas` and `maxPriorityFeePerGas` are set to `0` they are treated as
37    ///   missing values, bypassing fee checks wrt. `baseFeePerGas`.
38    ///
39    /// This mirrors geth's behaviour when transaction requests are executed: <https://github.com/ethereum/go-ethereum/blob/380688c636a654becc8f114438c2a5d93d2db032/core/state_transition.go#L306-L306>
40    ///
41    /// [`BlockEnv`]: revm_context::BlockEnv
42    pub fn ensure_fees(
43        call_gas_price: Option<U256>,
44        call_max_fee: Option<U256>,
45        call_priority_fee: Option<U256>,
46        block_base_fee: U256,
47        blob_versioned_hashes: Option<&[B256]>,
48        max_fee_per_blob_gas: Option<U256>,
49        block_blob_fee: Option<U256>,
50    ) -> Result<Self, CallFeesError> {
51        /// Get the effective gas price of a transaction as specfified in EIP-1559 with relevant
52        /// checks.
53        fn get_effective_gas_price(
54            max_fee_per_gas: Option<U256>,
55            max_priority_fee_per_gas: Option<U256>,
56            block_base_fee: U256,
57        ) -> Result<U256, CallFeesError> {
58            match max_fee_per_gas {
59                Some(max_fee) => {
60                    let max_priority_fee_per_gas = max_priority_fee_per_gas.unwrap_or(U256::ZERO);
61
62                    // only enforce the fee cap if provided input is not zero
63                    if !(max_fee.is_zero() && max_priority_fee_per_gas.is_zero()) &&
64                        max_fee < block_base_fee
65                    {
66                        // `base_fee_per_gas` is greater than the `max_fee_per_gas`
67                        return Err(CallFeesError::FeeCapTooLow)
68                    }
69                    if max_fee < max_priority_fee_per_gas {
70                        return Err(
71                            // `max_priority_fee_per_gas` is greater than the `max_fee_per_gas`
72                            CallFeesError::TipAboveFeeCap,
73                        )
74                    }
75                    // ref <https://github.com/ethereum/go-ethereum/blob/0dd173a727dd2d2409b8e401b22e85d20c25b71f/internal/ethapi/transaction_args.go#L446-L446>
76                    Ok(min(
77                        max_fee,
78                        block_base_fee
79                            .checked_add(max_priority_fee_per_gas)
80                            .ok_or(CallFeesError::TipVeryHigh)?,
81                    ))
82                }
83                None => Ok(block_base_fee
84                    .checked_add(max_priority_fee_per_gas.unwrap_or(U256::ZERO))
85                    .ok_or(CallFeesError::TipVeryHigh)?),
86            }
87        }
88
89        let has_blob_hashes =
90            blob_versioned_hashes.as_ref().map(|blobs| !blobs.is_empty()).unwrap_or(false);
91
92        match (call_gas_price, call_max_fee, call_priority_fee, max_fee_per_blob_gas) {
93            (gas_price, None, None, None) => {
94                // either legacy transaction or no fee fields are specified
95                // when no fields are specified, set gas price to zero
96                let gas_price = gas_price.unwrap_or(U256::ZERO);
97                Ok(Self {
98                    gas_price,
99                    max_priority_fee_per_gas: None,
100                    max_fee_per_blob_gas: has_blob_hashes.then_some(block_blob_fee).flatten(),
101                })
102            }
103            (None, max_fee_per_gas, max_priority_fee_per_gas, None) => {
104                // request for eip-1559 transaction
105                let effective_gas_price = get_effective_gas_price(
106                    max_fee_per_gas,
107                    max_priority_fee_per_gas,
108                    block_base_fee,
109                )?;
110                let max_fee_per_blob_gas = has_blob_hashes.then_some(block_blob_fee).flatten();
111
112                Ok(Self {
113                    gas_price: effective_gas_price,
114                    max_priority_fee_per_gas,
115                    max_fee_per_blob_gas,
116                })
117            }
118            (None, max_fee_per_gas, max_priority_fee_per_gas, Some(max_fee_per_blob_gas)) => {
119                // request for eip-4844 transaction
120                let effective_gas_price = get_effective_gas_price(
121                    max_fee_per_gas,
122                    max_priority_fee_per_gas,
123                    block_base_fee,
124                )?;
125                // Ensure blob_hashes are present
126                if !has_blob_hashes {
127                    // Blob transaction but no blob hashes
128                    return Err(CallFeesError::BlobTransactionMissingBlobHashes)
129                }
130
131                Ok(Self {
132                    gas_price: effective_gas_price,
133                    max_priority_fee_per_gas,
134                    max_fee_per_blob_gas: Some(max_fee_per_blob_gas),
135                })
136            }
137            _ => {
138                // this fallback covers incompatible combinations of fields
139                Err(CallFeesError::ConflictingFeeFieldsInRequest)
140            }
141        }
142    }
143}
144
145/// Error coming from decoding and validating transaction request fees.
146#[derive(Debug, Error)]
147pub enum CallFeesError {
148    /// Thrown when a call or transaction request (`eth_call`, `eth_estimateGas`,
149    /// `eth_sendTransaction`) contains conflicting fields (legacy, EIP-1559)
150    #[error("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")]
151    ConflictingFeeFieldsInRequest,
152    /// Thrown post London if the transaction's fee is less than the base fee of the block
153    #[error("max fee per gas less than block base fee")]
154    FeeCapTooLow,
155    /// Thrown to ensure no one is able to specify a transaction with a tip higher than the total
156    /// fee cap.
157    #[error("max priority fee per gas higher than max fee per gas")]
158    TipAboveFeeCap,
159    /// A sanity error to avoid huge numbers specified in the tip field.
160    #[error("max priority fee per gas higher than 2^256-1")]
161    TipVeryHigh,
162    /// Blob transaction has no versioned hashes
163    #[error("blob transaction missing blob hashes")]
164    BlobTransactionMissingBlobHashes,
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use alloy_consensus::constants::GWEI_TO_WEI;
171
172    #[test]
173    fn test_ensure_0_fallback() {
174        let CallFees { gas_price, .. } =
175            CallFees::ensure_fees(None, None, None, U256::from(99), None, None, Some(U256::ZERO))
176                .unwrap();
177        assert!(gas_price.is_zero());
178    }
179
180    #[test]
181    fn test_ensure_max_fee_0_exception() {
182        let CallFees { gas_price, .. } =
183            CallFees::ensure_fees(None, Some(U256::ZERO), None, U256::from(99), None, None, None)
184                .unwrap();
185        assert!(gas_price.is_zero());
186    }
187
188    #[test]
189    fn test_blob_fees() {
190        let CallFees { gas_price, max_fee_per_blob_gas, .. } =
191            CallFees::ensure_fees(None, None, None, U256::from(99), None, None, Some(U256::ZERO))
192                .unwrap();
193        assert!(gas_price.is_zero());
194        assert_eq!(max_fee_per_blob_gas, None);
195
196        let CallFees { gas_price, max_fee_per_blob_gas, .. } = CallFees::ensure_fees(
197            None,
198            None,
199            None,
200            U256::from(99),
201            Some(&[B256::from(U256::ZERO)]),
202            None,
203            Some(U256::from(99)),
204        )
205        .unwrap();
206        assert!(gas_price.is_zero());
207        assert_eq!(max_fee_per_blob_gas, Some(U256::from(99)));
208    }
209
210    #[test]
211    fn test_eip_1559_fees() {
212        let CallFees { gas_price, .. } = CallFees::ensure_fees(
213            None,
214            Some(U256::from(25 * GWEI_TO_WEI)),
215            Some(U256::from(15 * GWEI_TO_WEI)),
216            U256::from(15 * GWEI_TO_WEI),
217            None,
218            None,
219            Some(U256::ZERO),
220        )
221        .unwrap();
222        assert_eq!(gas_price, U256::from(25 * GWEI_TO_WEI));
223
224        let CallFees { gas_price, .. } = CallFees::ensure_fees(
225            None,
226            Some(U256::from(25 * GWEI_TO_WEI)),
227            Some(U256::from(5 * GWEI_TO_WEI)),
228            U256::from(15 * GWEI_TO_WEI),
229            None,
230            None,
231            Some(U256::ZERO),
232        )
233        .unwrap();
234        assert_eq!(gas_price, U256::from(20 * GWEI_TO_WEI));
235
236        let CallFees { gas_price, .. } = CallFees::ensure_fees(
237            None,
238            Some(U256::from(30 * GWEI_TO_WEI)),
239            Some(U256::from(30 * GWEI_TO_WEI)),
240            U256::from(15 * GWEI_TO_WEI),
241            None,
242            None,
243            Some(U256::ZERO),
244        )
245        .unwrap();
246        assert_eq!(gas_price, U256::from(30 * GWEI_TO_WEI));
247
248        let call_fees = CallFees::ensure_fees(
249            None,
250            Some(U256::from(30 * GWEI_TO_WEI)),
251            Some(U256::from(31 * GWEI_TO_WEI)),
252            U256::from(15 * GWEI_TO_WEI),
253            None,
254            None,
255            Some(U256::ZERO),
256        );
257        assert!(call_fees.is_err());
258
259        let call_fees = CallFees::ensure_fees(
260            None,
261            Some(U256::from(5 * GWEI_TO_WEI)),
262            Some(U256::from(GWEI_TO_WEI)),
263            U256::from(15 * GWEI_TO_WEI),
264            None,
265            None,
266            Some(U256::ZERO),
267        );
268        assert!(call_fees.is_err());
269
270        let call_fees = CallFees::ensure_fees(
271            None,
272            Some(U256::MAX),
273            Some(U256::MAX),
274            U256::from(5 * GWEI_TO_WEI),
275            None,
276            None,
277            Some(U256::ZERO),
278        );
279        assert!(call_fees.is_err());
280    }
281}