@@ -8,7 +8,7 @@ use super::dispatch::{
88 MethodInfo , RouteItems , StateMutability , boundary_size_check, generate_param_decoding,
99 generate_revert_encoding_boundary, generate_router,
1010} ;
11- use super :: storage_layout:: extract_optional_slot_attr;
11+ use super :: storage_layout:: { SlotAttr , extract_optional_slot_attr} ;
1212use crate :: signature:: { SolType , compute_selector} ;
1313use crate :: utils:: { compute_function_signature, to_snake_case} ;
1414
@@ -208,6 +208,13 @@ pub(super) enum Slot {
208208 /// (`PACKED_BYTES == 32`); sub-word types must use auto-numbering.
209209 /// See [`SlotField`] for the rationale.
210210 Explicit ( u64 ) ,
211+ /// Explicit `#[slot(raw = EXPR)]`: bind the field to a fixed,
212+ /// externally-known 32-byte slot (e.g. an EIP-1967 proxy slot), outside the
213+ /// compiler-assigned sequential range. Unlike numeric `#[slot(N)]` this
214+ /// accepts sub-word types and places them right-aligned (`offset =
215+ /// 32 - PACKED_BYTES`) to match solc, since a pseudo-random external slot
216+ /// has no sibling fields to pack with.
217+ ExplicitRaw ( syn:: Expr ) ,
211218 /// Auto-numbered: position among auto-numbered fields is taken from
212219 /// declaration order during the slot-chain build. Packs sub-word
213220 /// siblings via `layout_step`.
@@ -262,11 +269,14 @@ fn auto_chain_fields(slot_fields: &[SlotField]) -> Vec<super::storage_layout::Ch
262269}
263270
264271impl SlotField {
265- /// Explicit slot value, or `None` if auto-numbered.
272+ /// Explicit *numeric* slot value, or `None` if auto-numbered or bound to a
273+ /// raw external slot. Raw slots return `None` because they live outside the
274+ /// compiler-assigned sequential range and don't participate in the
275+ /// numeric overlap / full-slot-only checks.
266276 pub ( super ) fn explicit_slot ( & self ) -> Option < u64 > {
267277 match self . slot {
268278 Slot :: Explicit ( n) => Some ( n) ,
269- Slot :: Auto => None ,
279+ Slot :: ExplicitRaw ( _ ) | Slot :: Auto => None ,
270280 }
271281 }
272282}
@@ -1298,19 +1308,35 @@ pub fn expand_contract(args: ContractArgs, input: ItemMod) -> syn::Result<TokenS
12981308 let name = & sf. name ;
12991309 let ty = & sf. ty ;
13001310 let cfgs = & sf. cfg_attrs ;
1301- let ( slot_expr , offset_expr, alone_expr) : ( TokenStream , TokenStream , TokenStream ) =
1302- match sf. slot {
1311+ let ( key_expr , offset_expr, alone_expr) : ( TokenStream , TokenStream , TokenStream ) =
1312+ match & sf. slot {
13031313 // Explicit `#[slot(N)]` is restricted to full-slot types
13041314 // elsewhere in this file (`explicit_slot_full_slot_only_checks`).
13051315 // Full-slot components ignore `alone`, so the literal `true`
13061316 // is purely cosmetic — it would behave identically as `false`
13071317 // for `Mapping`/`Lazy<U256>`/etc.
1308- Slot :: Explicit ( n) => ( quote ! { #n } , quote ! { 0u8 } , quote ! { true } ) ,
1318+ Slot :: Explicit ( n) => (
1319+ quote ! { :: pvm_contract_sdk:: StorageKey :: from_slot( #n) } ,
1320+ quote ! { 0u8 } ,
1321+ quote ! { true } ,
1322+ ) ,
1323+ // `#[slot(raw = EXPR)]` binds the field to a fixed external
1324+ // 32-byte slot. Sub-word types are placed right-aligned at
1325+ // `32 - PACKED_BYTES` (matching solc), and `alone = true`
1326+ // because a pseudo-random external slot has no sibling
1327+ // sub-word fields to preserve on write.
1328+ Slot :: ExplicitRaw ( expr) => (
1329+ quote ! { :: pvm_contract_sdk:: StorageKey :: from_raw( #expr) } ,
1330+ quote ! {
1331+ ( 32 - <#ty as :: pvm_contract_sdk:: StorageComponent >:: PACKED_BYTES ) as u8
1332+ } ,
1333+ quote ! { true } ,
1334+ ) ,
13091335 Slot :: Auto => {
13101336 let const_ident = quote:: format_ident!( "{}{}" , AUTO_SLOT_PREFIX , name) ;
13111337 let alone_ident = quote:: format_ident!( "{}{}" , AUTO_ALONE_PREFIX , name) ;
13121338 (
1313- quote ! { #const_ident. slot } ,
1339+ quote ! { :: pvm_contract_sdk :: StorageKey :: from_slot ( #const_ident. slot) } ,
13141340 quote ! { #const_ident. offset } ,
13151341 quote ! { #alone_ident } ,
13161342 )
@@ -1319,7 +1345,7 @@ pub fn expand_contract(args: ContractArgs, input: ItemMod) -> syn::Result<TokenS
13191345 quote ! {
13201346 #( #cfgs) *
13211347 #name: <#ty as :: pvm_contract_sdk:: StorageComponent >:: new_at(
1322- :: pvm_contract_sdk :: StorageKey :: from_slot ( #slot_expr ) ,
1348+ #key_expr ,
13231349 #offset_expr,
13241350 #alone_expr,
13251351 host. clone( ) ,
@@ -1853,7 +1879,7 @@ fn extract_slot_fields_from_struct(item_struct: &syn::ItemStruct) -> syn::Result
18531879 struct Raw {
18541880 name : Ident ,
18551881 ty : syn:: Type ,
1856- explicit : Option < u64 > ,
1882+ explicit : Option < SlotAttr > ,
18571883 cfg_attrs : Vec < syn:: Attribute > ,
18581884 original_field : syn:: Field ,
18591885 }
@@ -1921,8 +1947,14 @@ fn extract_slot_fields_from_struct(item_struct: &syn::ItemStruct) -> syn::Result
19211947
19221948 let mut fields = Vec :: new ( ) ;
19231949 for raw in raws {
1924- let slot = if let Some ( n) = raw. explicit {
1925- Slot :: Explicit ( n)
1950+ let slot = if let Some ( attr) = raw. explicit {
1951+ match attr {
1952+ SlotAttr :: Numeric ( n) => Slot :: Explicit ( n) ,
1953+ // Raw external slots carry an unevaluated `[u8; 32]` expression;
1954+ // like numeric explicit slots they may carry `#[cfg]` (they don't
1955+ // shift any later field's slot), so no cfg gate here.
1956+ SlotAttr :: Raw ( expr) => Slot :: ExplicitRaw ( expr) ,
1957+ }
19261958 } else {
19271959 // Auto-numbered fields share a const chain across slot consts;
19281960 // a #[cfg]-disabled field in the middle of the chain would break
@@ -3262,6 +3294,59 @@ mod tests {
32623294 ) ;
32633295 }
32643296
3297+ #[ test]
3298+ fn raw_slot_field_constructs_from_raw_key_right_aligned ( ) {
3299+ let item: ItemMod = syn:: parse_str (
3300+ r#"
3301+ mod my_proxy {
3302+ const IMPL_SLOT: [u8; 32] = [0u8; 32];
3303+ pub struct MyProxy {
3304+ #[slot(raw = IMPL_SLOT)]
3305+ impl_addr: Lazy<Address>,
3306+ }
3307+ impl MyProxy {
3308+ #[pvm_contract_macros::constructor]
3309+ pub fn new(&mut self) {}
3310+
3311+ #[pvm_contract_macros::method]
3312+ pub fn implementation(&self) -> Address {
3313+ self.impl_addr.get()
3314+ }
3315+ }
3316+ }
3317+ "# ,
3318+ )
3319+ . unwrap ( ) ;
3320+
3321+ let output = expand_contract ( ContractArgs :: default ( ) , item)
3322+ . unwrap ( )
3323+ . to_string ( ) ;
3324+
3325+ // Raw-slot fields bind to StorageKey::from_raw(EXPR), not from_slot.
3326+ assert ! (
3327+ output. contains( ":: StorageKey :: from_raw (IMPL_SLOT)" ) ,
3328+ "raw slot field should pass StorageKey::from_raw(IMPL_SLOT).\n \
3329+ Expanded output:\n {output}"
3330+ ) ;
3331+ assert ! (
3332+ !output. contains( "from_slot" ) ,
3333+ "a raw-only contract should emit no from_slot calls.\n \
3334+ Expanded output:\n {output}"
3335+ ) ;
3336+ // Sub-word types are placed right-aligned at 32 - PACKED_BYTES.
3337+ assert ! (
3338+ output. contains( "(32 - < Lazy < Address > as :: pvm_contract_sdk :: StorageComponent > :: PACKED_BYTES) as u8" ) ,
3339+ "raw sub-word field should be right-aligned at 32 - PACKED_BYTES.\n \
3340+ Expanded output:\n {output}"
3341+ ) ;
3342+ // The #[slot(raw = ...)] attribute must be stripped from the struct.
3343+ assert ! (
3344+ !output. contains( "# [slot" ) ,
3345+ "Slot attributes should be stripped from the struct output.\n \
3346+ Expanded output:\n {output}"
3347+ ) ;
3348+ }
3349+
32653350 #[ test]
32663351 fn slot_fields_initialize_in_deploy_and_call ( ) {
32673352 let item: ItemMod = syn:: parse_str (
0 commit comments