alloy_consensus/transaction/
eip7702.rs

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