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