Skip to main content

alloy_eip8141/
frame.rs

1use alloy_primitives::{Address, Bytes, U256};
2use alloy_rlp::{RlpDecodable, RlpEncodable};
3
4use crate::FrameAddress;
5
6/// EIP-8141 frame execution mode.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
8#[repr(u8)]
9#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
10#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
11#[cfg_attr(feature = "borsh", borsh(use_discriminant = true))]
12pub enum FrameMode {
13    /// Execute the frame as the protocol entry point.
14    #[default]
15    Default = 0,
16    /// Execute transaction validation.
17    Verify = 1,
18    /// Execute as the transaction sender.
19    Sender = 2,
20}
21
22impl FrameMode {
23    /// Returns true if this is [`Self::Default`].
24    pub const fn is_default(self) -> bool {
25        matches!(self, Self::Default)
26    }
27
28    /// Returns true if this is [`Self::Verify`].
29    pub const fn is_verify(self) -> bool {
30        matches!(self, Self::Verify)
31    }
32
33    /// Returns true if this is [`Self::Sender`].
34    pub const fn is_sender(self) -> bool {
35        matches!(self, Self::Sender)
36    }
37
38    /// Attempts to convert a raw mode byte into a [`FrameMode`].
39    pub const fn try_from_u8(value: u8) -> Option<Self> {
40        match value {
41            0 => Some(Self::Default),
42            1 => Some(Self::Verify),
43            2 => Some(Self::Sender),
44            _ => None,
45        }
46    }
47}
48
49impl_u8_discriminant!(FrameMode, InvalidMode, "invalid EIP-8141 frame mode");
50
51/// EIP-8141 approval scope.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
53#[repr(u8)]
54#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
55#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
56#[cfg_attr(feature = "borsh", borsh(use_discriminant = true))]
57pub enum ApprovalScope {
58    /// No approval scope.
59    #[default]
60    None = 0x00,
61    /// Approves gas payment.
62    Payment = 0x01,
63    /// Approves execution as the sender.
64    Execution = 0x02,
65    /// Approves both execution and gas payment.
66    ExecutionAndPayment = 0x03,
67}
68
69impl ApprovalScope {
70    /// Returns true if this is [`Self::None`].
71    pub const fn is_none(self) -> bool {
72        matches!(self, Self::None)
73    }
74
75    /// Returns true if this is [`Self::Payment`].
76    pub const fn is_payment(self) -> bool {
77        matches!(self, Self::Payment)
78    }
79
80    /// Returns true if this is [`Self::Execution`].
81    pub const fn is_execution(self) -> bool {
82        matches!(self, Self::Execution)
83    }
84
85    /// Returns true if this is [`Self::ExecutionAndPayment`].
86    pub const fn is_execution_and_payment(self) -> bool {
87        matches!(self, Self::ExecutionAndPayment)
88    }
89
90    /// Returns true if this scope allows execution approval, including
91    /// [`Self::ExecutionAndPayment`].
92    pub const fn allows_execution(self) -> bool {
93        matches!(self, Self::Execution | Self::ExecutionAndPayment)
94    }
95
96    /// Returns true if this scope allows payment approval, including
97    /// [`Self::ExecutionAndPayment`].
98    pub const fn allows_payment(self) -> bool {
99        matches!(self, Self::Payment | Self::ExecutionAndPayment)
100    }
101
102    /// Attempts to convert a raw scope byte into an [`ApprovalScope`].
103    pub const fn try_from_u8(value: u8) -> Option<Self> {
104        match value {
105            0x00 => Some(Self::None),
106            0x01 => Some(Self::Payment),
107            0x02 => Some(Self::Execution),
108            0x03 => Some(Self::ExecutionAndPayment),
109            _ => None,
110        }
111    }
112}
113
114impl_u8_discriminant!(ApprovalScope, InvalidScope);
115
116/// The independent execution and state gas budgets carried by an EIP-8141 frame.
117#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable)]
118#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
121#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
122pub struct FrameLimits {
123    /// Maximum execution gas available to the frame.
124    #[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::quantity"))]
125    pub execution: u64,
126    /// Maximum state gas available to the frame.
127    #[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::quantity"))]
128    pub state: u64,
129}
130
131/// A single EIP-8141 transaction frame.
132#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable)]
133#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
136#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
137pub struct Frame {
138    /// The frame execution mode.
139    pub mode: FrameMode,
140    /// Frame flags. Bits 0-1 encode approval scope, bit 2 encodes atomic batching.
141    #[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::quantity_u8"))]
142    pub flags: u8,
143    /// Target account. An empty address resolves to the transaction sender.
144    pub target: FrameAddress,
145    /// Independent execution and state gas limits for this frame.
146    pub limits: FrameLimits,
147    /// Wei value transferred by this frame. Non-zero value is valid only for `SENDER` frames.
148    pub value: U256,
149    /// Calldata provided to the top-level frame call.
150    pub data: Bytes,
151}
152
153impl Frame {
154    /// Creates a new frame from raw field values, including both gas limits.
155    pub const fn new(
156        mode: FrameMode,
157        flags: u8,
158        target: FrameAddress,
159        limits: FrameLimits,
160        value: U256,
161        data: Bytes,
162    ) -> Self {
163        Self { mode, flags, target, limits, value, data }
164    }
165
166    /// Returns the target address, or `None` when the frame resolves to the transaction sender.
167    pub const fn target_address(&self) -> Option<Address> {
168        self.target.address()
169    }
170
171    /// Resolves the target, substituting the transaction sender for an empty target.
172    pub const fn resolved_target(&self, sender: Address) -> Address {
173        self.target.resolve(sender)
174    }
175
176    /// Returns the allowed approval scope encoded in this frame's flags.
177    pub const fn allowed_scope(&self) -> ApprovalScope {
178        match self.flags & crate::APPROVE_SCOPE_MASK {
179            0 => ApprovalScope::None,
180            1 => ApprovalScope::Payment,
181            2 => ApprovalScope::Execution,
182            _ => ApprovalScope::ExecutionAndPayment,
183        }
184    }
185
186    /// Returns true if this frame has the atomic batch flag set.
187    pub const fn is_atomic_batch(&self) -> bool {
188        self.flags & crate::ATOMIC_BATCH_FLAG != 0
189    }
190
191    /// Returns true if any reserved flag bit is set, which makes the transaction invalid.
192    pub const fn has_reserved_flags(&self) -> bool {
193        self.flags & !crate::FRAME_FLAGS_MASK != 0
194    }
195
196    /// Returns true if this frame is an expiry verifier frame.
197    ///
198    /// Classification depends only on the frame mode and target. Call
199    /// [`Self::has_valid_expiry_verifier_fields`] separately when validating the transaction.
200    pub fn is_expiry_verifier(&self) -> bool {
201        self.mode == FrameMode::Verify && self.target_address() == Some(crate::EXPIRY_VERIFIER)
202    }
203
204    /// Returns true if the constrained fields of an expiry verifier frame are valid.
205    pub fn has_valid_expiry_verifier_fields(&self) -> bool {
206        self.flags == 0
207            && self.limits.state == 0
208            && self.value.is_zero()
209            && self.data.len() == crate::EXPIRY_DATA_LENGTH
210    }
211}
212
213/// Fee parameters carried by an EIP-8141 transaction.
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable)]
215#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
218#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
219pub struct TransactionFees {
220    /// Maximum priority fee per gas.
221    pub max_priority_fee_per_gas: U256,
222    /// Maximum total fee per gas.
223    pub max_fee_per_gas: U256,
224    /// Maximum fee per blob gas.
225    pub max_fee_per_blob_gas: U256,
226}
227
228#[cfg(test)]
229mod tests {
230    use super::{Frame, FrameMode};
231    use alloy_primitives::{Bytes, U256};
232
233    #[test]
234    fn approval_permissions_from_frame_flags() {
235        for (scope, execution, payment) in
236            [(0, false, false), (1, false, true), (2, true, false), (3, true, true)]
237        {
238            for atomic in [0, crate::ATOMIC_BATCH_FLAG] {
239                let frame = Frame { flags: scope | atomic, ..Default::default() };
240                assert_eq!(frame.allowed_scope().allows_execution(), execution);
241                assert_eq!(frame.allowed_scope().allows_payment(), payment);
242            }
243        }
244    }
245
246    fn expiry_frame() -> Frame {
247        Frame {
248            mode: FrameMode::Verify,
249            target: crate::EXPIRY_VERIFIER.into(),
250            data: Bytes::from(vec![0; crate::EXPIRY_DATA_LENGTH]),
251            ..Default::default()
252        }
253    }
254
255    #[test]
256    fn expiry_verifier_classification_is_separate_from_field_validation() {
257        let mut frame = expiry_frame();
258        assert!(frame.is_expiry_verifier());
259        assert!(frame.has_valid_expiry_verifier_fields());
260
261        frame.flags = 1;
262        frame.value = U256::from(1);
263        frame.data = Bytes::new();
264
265        assert!(frame.is_expiry_verifier());
266        assert!(!frame.has_valid_expiry_verifier_fields());
267    }
268}