Skip to main content

tempo_precompiles_macros/
lib.rs

1//! Procedural macros for generating type-safe EVM storage accessors.
2//!
3//! This crate provides:
4//! - `#[contract]` macro that transforms a storage schema into a fully-functional contract
5//! - `#[derive(Storable)]` macro for multi-slot storage structs
6//! - `storable_alloy_ints!` macro for generating alloy integer storage implementations
7//! - `storable_alloy_bytes!` macro for generating alloy FixedBytes storage implementations
8//! - `storable_rust_ints!` macro for generating standard Rust integer storage implementations
9
10mod layout;
11mod packing;
12mod storable;
13mod storable_primitives;
14mod storable_tests;
15mod utils;
16
17use alloy::primitives::U256;
18use proc_macro::TokenStream;
19use quote::quote;
20use syn::{
21    Data, DeriveInput, Expr, Fields, Ident, Token, Type, Visibility,
22    parse::{Parse, ParseStream, Parser},
23    parse_macro_input,
24    punctuated::Punctuated,
25};
26
27use crate::utils::extract_attributes;
28
29/// Configuration parsed from `#[contract(...)]` attribute arguments.
30struct ContractConfig {
31    /// Optional address expression for generating `Self::new()` and `Default`.
32    address: Option<Expr>,
33}
34
35impl Parse for ContractConfig {
36    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
37        if input.is_empty() {
38            return Ok(Self { address: None });
39        }
40
41        let ident: Ident = input.parse()?;
42        if ident != "addr" && ident != "address" {
43            return Err(syn::Error::new(
44                ident.span(),
45                "only `addr` attribute is supported",
46            ));
47        }
48
49        input.parse::<Token![=]>()?;
50        let address: Expr = input.parse()?;
51
52        Ok(Self {
53            address: Some(address),
54        })
55    }
56}
57
58const RESERVED: &[&str] = &["address", "storage", "msg_sender"];
59
60/// Transforms a struct that represents a storage layout into a contract with helper methods to
61/// easily interact with the EVM storage.
62/// Its packing and encoding schemes aim to be an exact representation of the storage model used by Solidity.
63///
64/// # Input: Storage Layout
65///
66/// ```ignore
67/// #[contract]
68/// pub struct TIP20Token {
69///     pub name: String,
70///     pub symbol: String,
71///     total_supply: U256,
72///     #[slot(10)]
73///     pub balances: Mapping<Address, U256>,
74///     #[slot(11)]
75///     pub allowances: Mapping<Address, Mapping<Address, U256>>,
76/// }
77/// ```
78///
79/// # Output: Contract with accessible storage via getter and setter methods.
80///
81/// The macro generates:
82/// 1. Transformed struct with generic parameters and runtime fields
83/// 2. Constructor: `__new(address, storage)`
84/// 3. Type-safe (private) getter and setter methods
85///
86/// # Requirements
87///
88/// - No duplicate slot assignments
89/// - Unique field names, excluding the reserved ones: `address`, `storage`, `msg_sender`.
90/// - All field types must implement `Storable`, and mapping keys must implement `StorageKey`.
91#[proc_macro_attribute]
92pub fn contract(attr: TokenStream, item: TokenStream) -> TokenStream {
93    let config = parse_macro_input!(attr as ContractConfig);
94    let input = parse_macro_input!(item as DeriveInput);
95
96    match gen_contract_output(input, config.address.as_ref()) {
97        Ok(tokens) => tokens.into(),
98        Err(err) => err.to_compile_error().into(),
99    }
100}
101
102/// Main code generation function with optional call trait generation
103fn gen_contract_output(
104    input: DeriveInput,
105    address: Option<&Expr>,
106) -> syn::Result<proc_macro2::TokenStream> {
107    let (ident, vis) = (input.ident.clone(), input.vis.clone());
108    let fields = parse_fields(input)?;
109
110    let storage_output = gen_contract_storage(&ident, &vis, &fields, address)?;
111    Ok(quote! { #storage_output })
112}
113
114/// Information extracted from a field in the storage schema
115#[derive(Debug)]
116struct FieldInfo {
117    name: Ident,
118    ty: Type,
119    slot: Option<U256>,
120    base_slot: Option<U256>,
121}
122
123/// Classification of a field based on its type
124#[derive(Debug, Clone, Copy)]
125enum FieldKind<'a> {
126    /// Fields with a direct slot allocation, either single or multi (`Slot<V>`).
127    Direct(&'a Type),
128    /// Mapping fields. Handles all nesting levels via recursive types.
129    Mapping { key: &'a Type, value: &'a Type },
130}
131
132fn parse_fields(input: DeriveInput) -> syn::Result<Vec<FieldInfo>> {
133    // Ensure no generic parameters on input
134    if !input.generics.params.is_empty() {
135        return Err(syn::Error::new_spanned(
136            &input.generics,
137            "Contract structs cannot have generic parameters",
138        ));
139    }
140
141    // Ensure struct with named fields
142    let named_fields = if let Data::Struct(data) = input.data
143        && let Fields::Named(fields) = data.fields
144    {
145        fields.named
146    } else {
147        return Err(syn::Error::new_spanned(
148            input.ident,
149            "Only structs with named fields are supported",
150        ));
151    };
152
153    // Parse extract attributes
154    named_fields
155        .into_iter()
156        .map(|field| {
157            let name = field
158                .ident
159                .as_ref()
160                .ok_or_else(|| syn::Error::new_spanned(&field, "Fields must have names"))?;
161
162            if RESERVED.contains(&name.to_string().as_str()) {
163                return Err(syn::Error::new_spanned(
164                    name,
165                    format!("Field name '{name}' is reserved"),
166                ));
167            }
168
169            let (slot, base_slot) = extract_attributes(&field.attrs)?;
170            Ok(FieldInfo {
171                name: name.to_owned(),
172                ty: field.ty,
173                slot,
174                base_slot,
175            })
176        })
177        .collect()
178}
179
180/// Main code generation function for storage accessors
181fn gen_contract_storage(
182    ident: &Ident,
183    vis: &Visibility,
184    fields: &[FieldInfo],
185    address: Option<&Expr>,
186) -> syn::Result<proc_macro2::TokenStream> {
187    // Generate the complete output
188    let allocated_fields = packing::allocate_slots(fields)?;
189    let transformed_struct = layout::gen_struct(ident, vis, &allocated_fields);
190    let storage_trait = layout::gen_contract_storage_impl(ident);
191    let constructor = layout::gen_constructor(ident, &allocated_fields, address);
192    let slots_module = layout::gen_slots_module(&allocated_fields);
193    let default_impl = if address.is_some() {
194        layout::gen_default_impl(ident)
195    } else {
196        proc_macro2::TokenStream::new()
197    };
198
199    let output = quote! {
200        #slots_module
201        #transformed_struct
202        #constructor
203        #storage_trait
204        #default_impl
205    };
206
207    Ok(output)
208}
209
210/// Derives the `Storable` trait for structs with named fields.
211///
212/// This macro generates implementations for loading and storing multi-slot
213/// struct layout in EVM storage.
214/// Its packing and encoding schemes aim to be an exact representation of
215/// the storage model used by Solidity.
216///
217/// # Requirements
218///
219/// - The struct must have named fields (not tuple structs or unit structs)
220/// - All fields must implement the `Storable` trait
221///
222/// # Generated Code
223///
224/// For each struct field, the macro generates sequential slot offsets.
225/// It implements the `Storable` trait methods:
226/// - `load` - Loads the struct from storage
227/// - `store` - Stores the struct to storage
228/// - `delete` - Uses default implementation (sets all slots to zero)
229///
230/// # Example
231///
232/// ```ignore
233/// use precompiles::storage::Storable;
234/// use alloy_primitives::{Address, U256};
235///
236/// #[derive(Storable)]
237/// pub struct RewardStream {
238///     pub funder: Address,              // rel slot: 0 (20 bytes)
239///     pub start_time: u64,              // rel slot: 0 (8 bytes)
240///     pub end_time: u64,                // rel slot: 1 (8 bytes)
241///     pub rate_per_second_scaled: U256, // rel slot: 2 (32 bytes)
242///     pub amount_total: U256,           // rel slot: 3 (32 bytes)
243/// }
244/// ```
245#[proc_macro_derive(Storable, attributes(storable_arrays))]
246pub fn derive_storage_block(input: TokenStream) -> TokenStream {
247    let input = parse_macro_input!(input as DeriveInput);
248
249    match storable::derive_impl(input) {
250        Ok(tokens) => tokens.into(),
251        Err(err) => err.to_compile_error().into(),
252    }
253}
254
255// -- STORAGE PRIMITIVES TRAIT IMPLEMENTATIONS -------------------------------------------
256
257/// Generate `StorableType` and `Storable` implementations for all standard integer types.
258///
259/// Generates implementations for all standard Rust integer types:
260/// u8/i8, u16/i16, u32/i32, u64/i64, u128/i128.
261///
262/// Each type gets:
263/// - `StorableType` impl with `BYTE_COUNT` constant
264/// - `Storable` impl with `load()`, `store()` methods
265/// - `StorageKey` impl for use as mapping keys
266/// - Auto-generated tests that verify round-trip conversions with random values
267#[proc_macro]
268pub fn storable_rust_ints(_input: TokenStream) -> TokenStream {
269    storable_primitives::gen_storable_rust_ints().into()
270}
271
272/// Generate `StorableType` and `Storable` implementations for alloy integer types.
273///
274/// Generates implementations for all alloy integer types (both signed and unsigned):
275/// U8/I8, U16/I16, U32/I32, U64/I64, U128/I128, U256/I256.
276///
277/// Each type gets:
278/// - `StorableType` impl with `BYTE_COUNT` constant
279/// - `Storable` impl with `load()`, `store()` methods
280/// - `StorageKey` impl for use as mapping keys
281/// - Auto-generated tests that verify round-trip conversions using alloy's `.random()` method
282#[proc_macro]
283pub fn storable_alloy_ints(_input: TokenStream) -> TokenStream {
284    storable_primitives::gen_storable_alloy_ints().into()
285}
286
287/// Generate `StorableType` and `Storable` implementations for alloy `FixedBytes<N>` types.
288///
289/// Generates implementations for all fixed-size byte arrays from `N = 1..32`
290/// All sizes fit within a single storage slot.
291///
292/// Each type gets:
293/// - `StorableType` impl with `BYTE_COUNT` constant
294/// - `Storable` impl with `load()`, `store()` methods
295/// - `StorageKey` impl for use as mapping keys
296/// - Auto-generated tests that verify round-trip conversions using alloy's `.random()` method
297///
298/// # Usage
299/// ```ignore
300/// storable_alloy_bytes!();
301/// ```
302#[proc_macro]
303pub fn storable_alloy_bytes(_input: TokenStream) -> TokenStream {
304    storable_primitives::gen_storable_alloy_bytes().into()
305}
306
307/// Generate comprehensive property tests for all storage types.
308///
309/// This macro generates:
310/// - Arbitrary function generators for all Rust and Alloy integer types
311/// - Arbitrary function generators for all `FixedBytes<N>` sizes `N = 1..32`
312/// - Property test invocations using the existing test body macros
313#[proc_macro]
314pub fn gen_storable_tests(_input: TokenStream) -> TokenStream {
315    storable_tests::gen_storable_tests().into()
316}
317
318/// Generate `Storable` implementations for fixed-size arrays of primitive types.
319///
320/// Generates implementations for arrays of sizes 1-32 for the following element types:
321/// - Rust integers: u8-u128, i8-i128
322/// - Alloy integers: U8-U256, I8-I256
323/// - Address
324/// - FixedBytes<20>, FixedBytes<32>
325///
326/// Each array gets:
327/// - `StorableType` impl with `LAYOUT = Layout::Slot`
328/// - `Storable`
329#[proc_macro]
330pub fn storable_arrays(_input: TokenStream) -> TokenStream {
331    storable_primitives::gen_storable_arrays().into()
332}
333
334/// Generate `Storable` implementations for nested arrays of small primitive types.
335///
336/// Generates implementations for nested arrays like `[[u8; 4]; 8]` where:
337/// - Inner arrays are small (2, 4, 8, 16 for u8; 2, 4, 8 for u16)
338/// - Total slot count ≤ 32
339#[proc_macro]
340pub fn storable_nested_arrays(_input: TokenStream) -> TokenStream {
341    storable_primitives::gen_nested_arrays().into()
342}
343
344// -- TEST HELPERS -------------------------------------------------------------
345
346/// Test helper macro for validating slots
347#[proc_macro]
348pub fn gen_test_fields_layout(input: TokenStream) -> TokenStream {
349    let input = proc_macro2::TokenStream::from(input);
350
351    // Parse comma-separated identifiers
352    let parser = syn::punctuated::Punctuated::<Ident, syn::Token![,]>::parse_terminated;
353    let idents = match parser.parse2(input) {
354        Ok(idents) => idents,
355        Err(err) => return err.to_compile_error().into(),
356    };
357
358    // Generate storage fields
359    let field_calls: Vec<_> = idents
360        .into_iter()
361        .map(|ident| {
362            let field_name = ident.to_string();
363            let const_name = field_name.to_uppercase();
364            let field_name = utils::to_camel_case(&field_name);
365            let slot_ident = Ident::new(&const_name, ident.span());
366            let offset_ident = Ident::new(&format!("{const_name}_OFFSET"), ident.span());
367            let bytes_ident = Ident::new(&format!("{const_name}_BYTES"), ident.span());
368
369            quote! {
370                RustStorageField::new(#field_name, slots::#slot_ident, slots::#offset_ident, slots::#bytes_ident)
371            }
372        })
373        .collect();
374
375    // Generate the final vec!
376    let output = quote! {
377        vec![#(#field_calls),*]
378    };
379
380    output.into()
381}
382
383/// Test helper macro for validating slots
384#[proc_macro]
385pub fn gen_test_fields_struct(input: TokenStream) -> TokenStream {
386    let input = proc_macro2::TokenStream::from(input);
387
388    // Parse comma-separated identifiers
389    let parser = |input: ParseStream<'_>| {
390        let base_slot: Expr = input.parse()?;
391        input.parse::<Token![,]>()?;
392        let fields = Punctuated::<Ident, Token![,]>::parse_terminated(input)?;
393        Ok((base_slot, fields))
394    };
395
396    let (base_slot, idents) = match Parser::parse2(parser, input) {
397        Ok(result) => result,
398        Err(err) => return err.to_compile_error().into(),
399    };
400
401    // Generate storage fields
402    let field_calls: Vec<_> = idents
403        .into_iter()
404        .map(|ident| {
405            let field_name = ident.to_string();
406            let const_name = field_name.to_uppercase();
407            let field_name = utils::to_camel_case(&field_name);
408            let slot_ident = Ident::new(&const_name, ident.span());
409            let offset_ident = Ident::new(&format!("{const_name}_OFFSET"), ident.span());
410            let loc_ident = Ident::new(&format!("{const_name}_LOC"), ident.span());
411            let bytes_ident = quote! {#loc_ident.size};
412
413            quote! {
414                RustStorageField::new(#field_name, #base_slot + #slot_ident, #offset_ident, #bytes_ident)
415            }
416        })
417        .collect();
418
419    // Generate the final vec!
420    let output = quote! {
421        vec![#(#field_calls),*]
422    };
423
424    output.into()
425}