alloy_consensus/transaction/
eip7702.rs

1use crate::{SignableTransaction, Transaction, TxType};
2use alloc::vec::Vec;
3use alloy_eips::{
4    eip2930::AccessList,
5    eip7702::{constants::EIP7702_TX_TYPE_ID, SignedAuthorization},
6    Typed2718,
7};
8use alloy_primitives::{
9    Address, Bytes, ChainId, PrimitiveSignature as Signature, TxKind, B256, U256,
10};
11use alloy_rlp::{BufMut, Decodable, Encodable};
12use core::mem;
13
14use super::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx};
15
16/// A transaction with a priority fee ([EIP-7702](https://eips.ethereum.org/EIPS/eip-7702)).
17#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
18#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
21#[doc(alias = "Eip7702Transaction", alias = "TransactionEip7702", alias = "Eip7702Tx")]
22pub struct TxEip7702 {
23    /// EIP-155: Simple replay attack protection
24    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
25    pub chain_id: ChainId,
26    /// A scalar value equal to the number of transactions sent by the sender; formally Tn.
27    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
28    pub nonce: u64,
29    /// A scalar value equal to the maximum
30    /// amount of gas that should be used in executing
31    /// this transaction. This is paid up-front, before any
32    /// computation is done and may not be increased
33    /// later; formally Tg.
34    #[cfg_attr(
35        feature = "serde",
36        serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
37    )]
38    pub gas_limit: u64,
39    /// A scalar value equal to the maximum
40    /// amount of gas that should be used in executing
41    /// this transaction. This is paid up-front, before any
42    /// computation is done and may not be increased
43    /// later; formally Tg.
44    ///
45    /// As ethereum circulation is around 120mil eth as of 2022 that is around
46    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
47    /// 340282366920938463463374607431768211455
48    ///
49    /// This is also known as `GasFeeCap`
50    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
51    pub max_fee_per_gas: u128,
52    /// Max Priority fee that transaction is paying
53    ///
54    /// As ethereum circulation is around 120mil eth as of 2022 that is around
55    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
56    /// 340282366920938463463374607431768211455
57    ///
58    /// This is also known as `GasTipCap`
59    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
60    pub max_priority_fee_per_gas: u128,
61    /// The 160-bit address of the message call’s recipient.
62    pub to: Address,
63    /// A scalar value equal to the number of Wei to
64    /// be transferred to the message call’s recipient or,
65    /// in the case of contract creation, as an endowment
66    /// to the newly created account; formally Tv.
67    pub value: U256,
68    /// The accessList specifies a list of addresses and storage keys;
69    /// these addresses and storage keys are added into the `accessed_addresses`
70    /// and `accessed_storage_keys` global sets (introduced in EIP-2929).
71    /// A gas cost is charged, though at a discount relative to the cost of
72    /// accessing outside the list.
73    pub access_list: AccessList,
74    /// Authorizations are used to temporarily set the code of its signer to
75    /// the code referenced by `address`. These also include a `chain_id` (which
76    /// can be set to zero and not evaluated) as well as an optional `nonce`.
77    pub authorization_list: Vec<SignedAuthorization>,
78    /// Input has two uses depending if transaction is Create or Call (if `to` field is None or
79    /// Some). pub init: An unlimited size byte array specifying the
80    /// EVM-code for the account initialisation procedure CREATE,
81    /// data: An unlimited size byte array specifying the
82    /// input data of the message call, formally Td.
83    pub input: Bytes,
84}
85
86impl TxEip7702 {
87    /// Get the transaction type.
88    #[doc(alias = "transaction_type")]
89    pub const fn tx_type() -> TxType {
90        TxType::Eip7702
91    }
92
93    /// Calculates a heuristic for the in-memory size of the [TxEip7702] transaction.
94    #[inline]
95    pub fn size(&self) -> usize {
96        mem::size_of::<ChainId>() + // chain_id
97        mem::size_of::<u64>() + // nonce
98        mem::size_of::<u64>() + // gas_limit
99        mem::size_of::<u128>() + // max_fee_per_gas
100        mem::size_of::<u128>() + // max_priority_fee_per_gas
101        mem::size_of::<Address>() + // to
102        mem::size_of::<U256>() + // value
103        self.access_list.size() + // access_list
104        self.input.len() + // input
105        self.authorization_list.capacity() * mem::size_of::<SignedAuthorization>()
106        // authorization_list
107    }
108}
109
110impl RlpEcdsaEncodableTx for TxEip7702 {
111    /// Outputs the length of the transaction's fields, without a RLP header.
112    #[doc(hidden)]
113    fn rlp_encoded_fields_length(&self) -> usize {
114        self.chain_id.length()
115            + self.nonce.length()
116            + self.max_priority_fee_per_gas.length()
117            + self.max_fee_per_gas.length()
118            + self.gas_limit.length()
119            + self.to.length()
120            + self.value.length()
121            + self.input.0.length()
122            + self.access_list.length()
123            + self.authorization_list.length()
124    }
125
126    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
127        self.chain_id.encode(out);
128        self.nonce.encode(out);
129        self.max_priority_fee_per_gas.encode(out);
130        self.max_fee_per_gas.encode(out);
131        self.gas_limit.encode(out);
132        self.to.encode(out);
133        self.value.encode(out);
134        self.input.0.encode(out);
135        self.access_list.encode(out);
136        self.authorization_list.encode(out);
137    }
138}
139
140impl RlpEcdsaDecodableTx for TxEip7702 {
141    const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
142
143    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
144        Ok(Self {
145            chain_id: Decodable::decode(buf)?,
146            nonce: Decodable::decode(buf)?,
147            max_priority_fee_per_gas: Decodable::decode(buf)?,
148            max_fee_per_gas: Decodable::decode(buf)?,
149            gas_limit: Decodable::decode(buf)?,
150            to: Decodable::decode(buf)?,
151            value: Decodable::decode(buf)?,
152            input: Decodable::decode(buf)?,
153            access_list: Decodable::decode(buf)?,
154            authorization_list: Decodable::decode(buf)?,
155        })
156    }
157}
158
159impl Transaction for TxEip7702 {
160    #[inline]
161    fn chain_id(&self) -> Option<ChainId> {
162        Some(self.chain_id)
163    }
164
165    #[inline]
166    fn nonce(&self) -> u64 {
167        self.nonce
168    }
169
170    #[inline]
171    fn gas_limit(&self) -> u64 {
172        self.gas_limit
173    }
174
175    #[inline]
176    fn gas_price(&self) -> Option<u128> {
177        None
178    }
179
180    #[inline]
181    fn max_fee_per_gas(&self) -> u128 {
182        self.max_fee_per_gas
183    }
184
185    #[inline]
186    fn max_priority_fee_per_gas(&self) -> Option<u128> {
187        Some(self.max_priority_fee_per_gas)
188    }
189
190    #[inline]
191    fn max_fee_per_blob_gas(&self) -> Option<u128> {
192        None
193    }
194
195    #[inline]
196    fn priority_fee_or_price(&self) -> u128 {
197        self.max_priority_fee_per_gas
198    }
199
200    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
201        base_fee.map_or(self.max_fee_per_gas, |base_fee| {
202            // if the tip is greater than the max priority fee per gas, set it to the max
203            // priority fee per gas + base fee
204            let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128);
205            if tip > self.max_priority_fee_per_gas {
206                self.max_priority_fee_per_gas + base_fee as u128
207            } else {
208                // otherwise return the max fee per gas
209                self.max_fee_per_gas
210            }
211        })
212    }
213
214    #[inline]
215    fn is_dynamic_fee(&self) -> bool {
216        true
217    }
218
219    #[inline]
220    fn kind(&self) -> TxKind {
221        self.to.into()
222    }
223
224    #[inline]
225    fn is_create(&self) -> bool {
226        false
227    }
228
229    #[inline]
230    fn value(&self) -> U256 {
231        self.value
232    }
233
234    #[inline]
235    fn input(&self) -> &Bytes {
236        &self.input
237    }
238
239    #[inline]
240    fn access_list(&self) -> Option<&AccessList> {
241        Some(&self.access_list)
242    }
243
244    #[inline]
245    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
246        None
247    }
248
249    #[inline]
250    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
251        Some(&self.authorization_list)
252    }
253}
254
255impl SignableTransaction<Signature> for TxEip7702 {
256    fn set_chain_id(&mut self, chain_id: ChainId) {
257        self.chain_id = chain_id;
258    }
259
260    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
261        out.put_u8(EIP7702_TX_TYPE_ID);
262        self.encode(out)
263    }
264
265    fn payload_len_for_signature(&self) -> usize {
266        self.length() + 1
267    }
268}
269
270impl Typed2718 for TxEip7702 {
271    fn ty(&self) -> u8 {
272        TxType::Eip7702 as u8
273    }
274}
275
276impl Encodable for TxEip7702 {
277    fn encode(&self, out: &mut dyn BufMut) {
278        self.rlp_encode(out);
279    }
280
281    fn length(&self) -> usize {
282        self.rlp_encoded_length()
283    }
284}
285
286impl Decodable for TxEip7702 {
287    fn decode(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
288        Self::rlp_decode(data)
289    }
290}
291
292/// Bincode-compatible [`TxEip7702`] serde implementation.
293#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
294pub(super) mod serde_bincode_compat {
295    use alloc::{borrow::Cow, vec::Vec};
296    use alloy_eips::{eip2930::AccessList, eip7702::serde_bincode_compat::SignedAuthorization};
297    use alloy_primitives::{Address, Bytes, ChainId, U256};
298    use serde::{Deserialize, Deserializer, Serialize, Serializer};
299    use serde_with::{DeserializeAs, SerializeAs};
300
301    /// Bincode-compatible [`super::TxEip7702`] serde implementation.
302    ///
303    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
304    /// ```rust
305    /// use alloy_consensus::{serde_bincode_compat, TxEip7702};
306    /// use serde::{Deserialize, Serialize};
307    /// use serde_with::serde_as;
308    ///
309    /// #[serde_as]
310    /// #[derive(Serialize, Deserialize)]
311    /// struct Data {
312    ///     #[serde_as(as = "serde_bincode_compat::transaction::TxEip7702")]
313    ///     transaction: TxEip7702,
314    /// }
315    /// ```
316    #[derive(Debug, Serialize, Deserialize)]
317    pub struct TxEip7702<'a> {
318        chain_id: ChainId,
319        nonce: u64,
320        gas_limit: u64,
321        max_fee_per_gas: u128,
322        max_priority_fee_per_gas: u128,
323        to: Address,
324        value: U256,
325        access_list: Cow<'a, AccessList>,
326        authorization_list: Vec<SignedAuthorization<'a>>,
327        input: Cow<'a, Bytes>,
328    }
329
330    impl<'a> From<&'a super::TxEip7702> for TxEip7702<'a> {
331        fn from(value: &'a super::TxEip7702) -> Self {
332            Self {
333                chain_id: value.chain_id,
334                nonce: value.nonce,
335                gas_limit: value.gas_limit,
336                max_fee_per_gas: value.max_fee_per_gas,
337                max_priority_fee_per_gas: value.max_priority_fee_per_gas,
338                to: value.to,
339                value: value.value,
340                access_list: Cow::Borrowed(&value.access_list),
341                authorization_list: value.authorization_list.iter().map(Into::into).collect(),
342                input: Cow::Borrowed(&value.input),
343            }
344        }
345    }
346
347    impl<'a> From<TxEip7702<'a>> for super::TxEip7702 {
348        fn from(value: TxEip7702<'a>) -> Self {
349            Self {
350                chain_id: value.chain_id,
351                nonce: value.nonce,
352                gas_limit: value.gas_limit,
353                max_fee_per_gas: value.max_fee_per_gas,
354                max_priority_fee_per_gas: value.max_priority_fee_per_gas,
355                to: value.to,
356                value: value.value,
357                access_list: value.access_list.into_owned(),
358                authorization_list: value.authorization_list.into_iter().map(Into::into).collect(),
359                input: value.input.into_owned(),
360            }
361        }
362    }
363
364    impl SerializeAs<super::TxEip7702> for TxEip7702<'_> {
365        fn serialize_as<S>(source: &super::TxEip7702, serializer: S) -> Result<S::Ok, S::Error>
366        where
367            S: Serializer,
368        {
369            TxEip7702::from(source).serialize(serializer)
370        }
371    }
372
373    impl<'de> DeserializeAs<'de, super::TxEip7702> for TxEip7702<'de> {
374        fn deserialize_as<D>(deserializer: D) -> Result<super::TxEip7702, D::Error>
375        where
376            D: Deserializer<'de>,
377        {
378            TxEip7702::deserialize(deserializer).map(Into::into)
379        }
380    }
381
382    #[cfg(test)]
383    mod tests {
384        use arbitrary::Arbitrary;
385        use rand::Rng;
386        use serde::{Deserialize, Serialize};
387        use serde_with::serde_as;
388
389        use super::super::{serde_bincode_compat, TxEip7702};
390
391        #[test]
392        fn test_tx_eip7702_bincode_roundtrip() {
393            #[serde_as]
394            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
395            struct Data {
396                #[serde_as(as = "serde_bincode_compat::TxEip7702")]
397                transaction: TxEip7702,
398            }
399
400            let mut bytes = [0u8; 1024];
401            rand::thread_rng().fill(bytes.as_mut_slice());
402            let data = Data {
403                transaction: TxEip7702::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
404                    .unwrap(),
405            };
406
407            let encoded = bincode::serialize(&data).unwrap();
408            let decoded: Data = bincode::deserialize(&encoded).unwrap();
409            assert_eq!(decoded, data);
410        }
411    }
412}
413
414#[cfg(all(test, feature = "k256"))]
415mod tests {
416    use super::*;
417    use crate::SignableTransaction;
418    use alloy_eips::eip2930::AccessList;
419    use alloy_primitives::{address, b256, hex, Address, PrimitiveSignature as Signature, U256};
420
421    #[test]
422    fn encode_decode_eip7702() {
423        let tx =  TxEip7702 {
424            chain_id: 1,
425            nonce: 0x42,
426            gas_limit: 44386,
427            to: address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6"),
428            value: U256::from(0_u64),
429            input:  hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(),
430            max_fee_per_gas: 0x4a817c800,
431            max_priority_fee_per_gas: 0x3b9aca00,
432            access_list: AccessList::default(),
433            authorization_list: vec![],
434        };
435
436        let sig = Signature::from_scalars_and_parity(
437            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
438            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
439            false,
440        );
441
442        let mut buf = vec![];
443        tx.rlp_encode_signed(&sig, &mut buf);
444        let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
445        assert_eq!(decoded, tx.into_signed(sig));
446    }
447
448    #[test]
449    fn test_decode_create() {
450        // tests that a contract creation tx encodes and decodes properly
451        let tx = TxEip7702 {
452            chain_id: 1u64,
453            nonce: 0,
454            max_fee_per_gas: 0x4a817c800,
455            max_priority_fee_per_gas: 0x3b9aca00,
456            gas_limit: 2,
457            to: Address::default(),
458            value: U256::ZERO,
459            input: vec![1, 2].into(),
460            access_list: Default::default(),
461            authorization_list: Default::default(),
462        };
463        let sig = Signature::from_scalars_and_parity(
464            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
465            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
466            false,
467        );
468        let mut buf = vec![];
469        tx.rlp_encode_signed(&sig, &mut buf);
470        let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
471        assert_eq!(decoded, tx.into_signed(sig));
472    }
473
474    #[test]
475    fn test_decode_call() {
476        let tx = TxEip7702 {
477            chain_id: 1u64,
478            nonce: 0,
479            max_fee_per_gas: 0x4a817c800,
480            max_priority_fee_per_gas: 0x3b9aca00,
481            gas_limit: 2,
482            to: Address::default(),
483            value: U256::ZERO,
484            input: vec![1, 2].into(),
485            access_list: Default::default(),
486            authorization_list: Default::default(),
487        };
488
489        let sig = Signature::from_scalars_and_parity(
490            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
491            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
492            false,
493        );
494
495        let mut buf = vec![];
496        tx.rlp_encode_signed(&sig, &mut buf);
497        let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
498        assert_eq!(decoded, tx.into_signed(sig));
499    }
500}