Skip to main content

reth_rpc_convert/
fees.rs

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