alloy_consensus/transaction/
eip7702.rs

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