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