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}