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 = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
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    /// An unlimited size byte array specifying the
79    /// input data of the message call, formally Td.
80    pub input: Bytes,
81}
82
83impl TxEip7702 {
84    /// Get the transaction type.
85    #[doc(alias = "transaction_type")]
86    pub const fn tx_type() -> TxType {
87        TxType::Eip7702
88    }
89
90    /// Calculates a heuristic for the in-memory size of the [TxEip7702] transaction.
91    #[inline]
92    pub fn size(&self) -> usize {
93        mem::size_of::<ChainId>() + // chain_id
94        mem::size_of::<u64>() + // nonce
95        mem::size_of::<u64>() + // gas_limit
96        mem::size_of::<u128>() + // max_fee_per_gas
97        mem::size_of::<u128>() + // max_priority_fee_per_gas
98        mem::size_of::<Address>() + // to
99        mem::size_of::<U256>() + // value
100        self.access_list.size() + // access_list
101        self.input.len() + // input
102        self.authorization_list.capacity() * mem::size_of::<SignedAuthorization>()
103        // authorization_list
104    }
105}
106
107impl RlpEcdsaEncodableTx for TxEip7702 {
108    /// Outputs the length of the transaction's fields, without a RLP header.
109    #[doc(hidden)]
110    fn rlp_encoded_fields_length(&self) -> usize {
111        self.chain_id.length()
112            + self.nonce.length()
113            + self.max_priority_fee_per_gas.length()
114            + self.max_fee_per_gas.length()
115            + self.gas_limit.length()
116            + self.to.length()
117            + self.value.length()
118            + self.input.0.length()
119            + self.access_list.length()
120            + self.authorization_list.length()
121    }
122
123    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
124        self.chain_id.encode(out);
125        self.nonce.encode(out);
126        self.max_priority_fee_per_gas.encode(out);
127        self.max_fee_per_gas.encode(out);
128        self.gas_limit.encode(out);
129        self.to.encode(out);
130        self.value.encode(out);
131        self.input.0.encode(out);
132        self.access_list.encode(out);
133        self.authorization_list.encode(out);
134    }
135}
136
137impl RlpEcdsaDecodableTx for TxEip7702 {
138    const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
139
140    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
141        Ok(Self {
142            chain_id: Decodable::decode(buf)?,
143            nonce: Decodable::decode(buf)?,
144            max_priority_fee_per_gas: Decodable::decode(buf)?,
145            max_fee_per_gas: Decodable::decode(buf)?,
146            gas_limit: Decodable::decode(buf)?,
147            to: Decodable::decode(buf)?,
148            value: Decodable::decode(buf)?,
149            input: Decodable::decode(buf)?,
150            access_list: Decodable::decode(buf)?,
151            authorization_list: Decodable::decode(buf)?,
152        })
153    }
154}
155
156impl Transaction for TxEip7702 {
157    #[inline]
158    fn chain_id(&self) -> Option<ChainId> {
159        Some(self.chain_id)
160    }
161
162    #[inline]
163    fn nonce(&self) -> u64 {
164        self.nonce
165    }
166
167    #[inline]
168    fn gas_limit(&self) -> u64 {
169        self.gas_limit
170    }
171
172    #[inline]
173    fn gas_price(&self) -> Option<u128> {
174        None
175    }
176
177    #[inline]
178    fn max_fee_per_gas(&self) -> u128 {
179        self.max_fee_per_gas
180    }
181
182    #[inline]
183    fn max_priority_fee_per_gas(&self) -> Option<u128> {
184        Some(self.max_priority_fee_per_gas)
185    }
186
187    #[inline]
188    fn max_fee_per_blob_gas(&self) -> Option<u128> {
189        None
190    }
191
192    #[inline]
193    fn priority_fee_or_price(&self) -> u128 {
194        self.max_priority_fee_per_gas
195    }
196
197    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
198        alloy_eips::eip1559::calc_effective_gas_price(
199            self.max_fee_per_gas,
200            self.max_priority_fee_per_gas,
201            base_fee,
202        )
203    }
204
205    #[inline]
206    fn is_dynamic_fee(&self) -> bool {
207        true
208    }
209
210    #[inline]
211    fn kind(&self) -> TxKind {
212        self.to.into()
213    }
214
215    #[inline]
216    fn is_create(&self) -> bool {
217        false
218    }
219
220    #[inline]
221    fn value(&self) -> U256 {
222        self.value
223    }
224
225    #[inline]
226    fn input(&self) -> &Bytes {
227        &self.input
228    }
229
230    #[inline]
231    fn access_list(&self) -> Option<&AccessList> {
232        Some(&self.access_list)
233    }
234
235    #[inline]
236    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
237        None
238    }
239
240    #[inline]
241    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
242        Some(&self.authorization_list)
243    }
244}
245
246impl SignableTransaction<Signature> for TxEip7702 {
247    fn set_chain_id(&mut self, chain_id: ChainId) {
248        self.chain_id = chain_id;
249    }
250
251    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
252        out.put_u8(EIP7702_TX_TYPE_ID);
253        self.encode(out)
254    }
255
256    fn payload_len_for_signature(&self) -> usize {
257        self.length() + 1
258    }
259}
260
261impl Typed2718 for TxEip7702 {
262    fn ty(&self) -> u8 {
263        TxType::Eip7702 as u8
264    }
265}
266
267impl IsTyped2718 for TxEip7702 {
268    fn is_type(type_id: u8) -> bool {
269        matches!(type_id, 0x04)
270    }
271}
272
273impl Encodable for TxEip7702 {
274    fn encode(&self, out: &mut dyn BufMut) {
275        self.rlp_encode(out);
276    }
277
278    fn length(&self) -> usize {
279        self.rlp_encoded_length()
280    }
281}
282
283impl Decodable for TxEip7702 {
284    fn decode(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
285        Self::rlp_decode(data)
286    }
287}
288
289/// Bincode-compatible [`TxEip7702`] serde implementation.
290#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
291pub(super) mod serde_bincode_compat {
292    use alloc::{borrow::Cow, vec::Vec};
293    use alloy_eips::{eip2930::AccessList, eip7702::serde_bincode_compat::SignedAuthorization};
294    use alloy_primitives::{Address, Bytes, ChainId, U256};
295    use serde::{Deserialize, Deserializer, Serialize, Serializer};
296    use serde_with::{DeserializeAs, SerializeAs};
297
298    /// Bincode-compatible [`super::TxEip7702`] serde implementation.
299    ///
300    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
301    /// ```rust
302    /// use alloy_consensus::{serde_bincode_compat, TxEip7702};
303    /// use serde::{Deserialize, Serialize};
304    /// use serde_with::serde_as;
305    ///
306    /// #[serde_as]
307    /// #[derive(Serialize, Deserialize)]
308    /// struct Data {
309    ///     #[serde_as(as = "serde_bincode_compat::transaction::TxEip7702")]
310    ///     transaction: TxEip7702,
311    /// }
312    /// ```
313    #[derive(Debug, Serialize, Deserialize)]
314    pub struct TxEip7702<'a> {
315        chain_id: ChainId,
316        nonce: u64,
317        gas_limit: u64,
318        max_fee_per_gas: u128,
319        max_priority_fee_per_gas: u128,
320        to: Address,
321        value: U256,
322        access_list: Cow<'a, AccessList>,
323        authorization_list: Vec<SignedAuthorization<'a>>,
324        input: Cow<'a, Bytes>,
325    }
326
327    impl<'a> From<&'a super::TxEip7702> for TxEip7702<'a> {
328        fn from(value: &'a super::TxEip7702) -> Self {
329            Self {
330                chain_id: value.chain_id,
331                nonce: value.nonce,
332                gas_limit: value.gas_limit,
333                max_fee_per_gas: value.max_fee_per_gas,
334                max_priority_fee_per_gas: value.max_priority_fee_per_gas,
335                to: value.to,
336                value: value.value,
337                access_list: Cow::Borrowed(&value.access_list),
338                authorization_list: value.authorization_list.iter().map(Into::into).collect(),
339                input: Cow::Borrowed(&value.input),
340            }
341        }
342    }
343
344    impl<'a> From<TxEip7702<'a>> for super::TxEip7702 {
345        fn from(value: TxEip7702<'a>) -> Self {
346            Self {
347                chain_id: value.chain_id,
348                nonce: value.nonce,
349                gas_limit: value.gas_limit,
350                max_fee_per_gas: value.max_fee_per_gas,
351                max_priority_fee_per_gas: value.max_priority_fee_per_gas,
352                to: value.to,
353                value: value.value,
354                access_list: value.access_list.into_owned(),
355                authorization_list: value.authorization_list.into_iter().map(Into::into).collect(),
356                input: value.input.into_owned(),
357            }
358        }
359    }
360
361    impl SerializeAs<super::TxEip7702> for TxEip7702<'_> {
362        fn serialize_as<S>(source: &super::TxEip7702, serializer: S) -> Result<S::Ok, S::Error>
363        where
364            S: Serializer,
365        {
366            TxEip7702::from(source).serialize(serializer)
367        }
368    }
369
370    impl<'de> DeserializeAs<'de, super::TxEip7702> for TxEip7702<'de> {
371        fn deserialize_as<D>(deserializer: D) -> Result<super::TxEip7702, D::Error>
372        where
373            D: Deserializer<'de>,
374        {
375            TxEip7702::deserialize(deserializer).map(Into::into)
376        }
377    }
378
379    #[cfg(test)]
380    mod tests {
381        use arbitrary::Arbitrary;
382        use bincode::config;
383        use rand::Rng;
384        use serde::{Deserialize, Serialize};
385        use serde_with::serde_as;
386
387        use super::super::{serde_bincode_compat, TxEip7702};
388
389        #[test]
390        fn test_tx_eip7702_bincode_roundtrip() {
391            #[serde_as]
392            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
393            struct Data {
394                #[serde_as(as = "serde_bincode_compat::TxEip7702")]
395                transaction: TxEip7702,
396            }
397
398            let mut bytes = [0u8; 1024];
399            rand::thread_rng().fill(bytes.as_mut_slice());
400            let data = Data {
401                transaction: TxEip7702::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
402                    .unwrap(),
403            };
404
405            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
406            let (decoded, _) =
407                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
408            assert_eq!(decoded, data);
409        }
410    }
411}
412
413#[cfg(all(test, feature = "k256"))]
414mod tests {
415    use super::*;
416    use crate::SignableTransaction;
417    use alloy_eips::eip2930::AccessList;
418    use alloy_primitives::{address, b256, hex, Address, Signature, U256};
419
420    #[test]
421    fn encode_decode_eip7702() {
422        let tx =  TxEip7702 {
423            chain_id: 1,
424            nonce: 0x42,
425            gas_limit: 44386,
426            to: address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6"),
427            value: U256::from(0_u64),
428            input:  hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(),
429            max_fee_per_gas: 0x4a817c800,
430            max_priority_fee_per_gas: 0x3b9aca00,
431            access_list: AccessList::default(),
432            authorization_list: vec![],
433        };
434
435        let sig = Signature::from_scalars_and_parity(
436            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
437            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
438            false,
439        );
440
441        let mut buf = vec![];
442        tx.rlp_encode_signed(&sig, &mut buf);
443        let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
444        assert_eq!(decoded, tx.into_signed(sig));
445    }
446
447    #[test]
448    fn test_decode_create() {
449        // tests that a contract creation tx encodes and decodes properly
450        let tx = TxEip7702 {
451            chain_id: 1u64,
452            nonce: 0,
453            max_fee_per_gas: 0x4a817c800,
454            max_priority_fee_per_gas: 0x3b9aca00,
455            gas_limit: 2,
456            to: Address::default(),
457            value: U256::ZERO,
458            input: vec![1, 2].into(),
459            access_list: Default::default(),
460            authorization_list: Default::default(),
461        };
462        let sig = Signature::from_scalars_and_parity(
463            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
464            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
465            false,
466        );
467        let mut buf = vec![];
468        tx.rlp_encode_signed(&sig, &mut buf);
469        let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
470        assert_eq!(decoded, tx.into_signed(sig));
471    }
472
473    #[test]
474    fn test_decode_call() {
475        let tx = TxEip7702 {
476            chain_id: 1u64,
477            nonce: 0,
478            max_fee_per_gas: 0x4a817c800,
479            max_priority_fee_per_gas: 0x3b9aca00,
480            gas_limit: 2,
481            to: Address::default(),
482            value: U256::ZERO,
483            input: vec![1, 2].into(),
484            access_list: Default::default(),
485            authorization_list: Default::default(),
486        };
487
488        let sig = Signature::from_scalars_and_parity(
489            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
490            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
491            false,
492        );
493
494        let mut buf = vec![];
495        tx.rlp_encode_signed(&sig, &mut buf);
496        let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
497        assert_eq!(decoded, tx.into_signed(sig));
498    }
499}