Skip to content

Add #[slot(raw = KEY)] for binding typed storage fields to fixed external (EIP-1967) slots#114

Open
filip-parity wants to merge 1 commit into
mainfrom
feat/storage-slot-eip1967
Open

Add #[slot(raw = KEY)] for binding typed storage fields to fixed external (EIP-1967) slots#114
filip-parity wants to merge 1 commit into
mainfrom
feat/storage-slot-eip1967

Conversation

@filip-parity

@filip-parity filip-parity commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Closes #105.

Reworked per @smiasojed's review: the SDK now exposes only the generic
mechanism for binding a typed storage field to a fixed, externally-known slot,
and the EIP-1967 constant + proxy logic live in an example - mirroring the
Stylus/OZ separation.

@filip-parity filip-parity force-pushed the feat/storage-slot-eip1967 branch from c876e47 to 5baa149 Compare June 29, 2026 08:29
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Benchmark Size Comparison (vs main)

Artifact Baseline Current Delta Status
allowlist_no-alloc.debug.polkavm 63984 63993 +0.01% OK
allowlist_no-alloc.release.polkavm 3168 3168 +0.00% OK
allowlist_with-alloc.debug.polkavm 75603 75613 +0.01% OK
allowlist_with-alloc.release.polkavm 3567 3567 +0.00% OK
amm_reserves_no-alloc.debug.polkavm 42895 42903 +0.02% OK
amm_reserves_no-alloc.release.polkavm 1949 1949 +0.00% OK
amm_reserves_with-alloc.debug.polkavm 54615 54615 +0.00% OK
amm_reserves_with-alloc.release.polkavm 2288 2288 +0.00% OK
fibonacci_builder-dsl.debug.polkavm 37240 37240 +0.00% OK
fibonacci_builder-dsl.release.polkavm 638 638 +0.00% OK
fibonacci_no-alloc.debug.polkavm 35852 35852 +0.00% OK
fibonacci_no-alloc.release.polkavm 595 595 +0.00% OK
fibonacci_with-alloc.debug.polkavm 46304 46304 +0.00% OK
fibonacci_with-alloc.release.polkavm 600 600 +0.00% OK
multi_builder-dsl.debug.polkavm 59387 59395 +0.01% OK
multi_builder-dsl.release.polkavm 3070 3070 +0.00% OK
multi_no-alloc.debug.polkavm 62100 62100 +0.00% OK
multi_no-alloc.release.polkavm 2968 2968 +0.00% OK
multi_with-alloc.debug.polkavm 73138 73138 +0.00% OK
multi_with-alloc.release.polkavm 3096 3096 +0.00% OK
mytoken_builder-dsl.debug.polkavm 56535 56535 +0.00% OK
mytoken_builder-dsl.release.polkavm 3715 3715 +0.00% OK
mytoken_no-alloc.debug.polkavm 60322 60322 +0.00% OK
mytoken_no-alloc.release.polkavm 2955 2955 +0.00% OK
mytoken_storage_no-alloc.debug.polkavm 66570 66578 +0.01% OK
mytoken_storage_no-alloc.release.polkavm 3087 3087 +0.00% OK
mytoken_storage_with-alloc.debug.polkavm 79200 79208 +0.01% OK
mytoken_storage_with-alloc.release.polkavm 4130 4130 +0.00% OK
mytoken_with-alloc.debug.polkavm 74407 74415 +0.01% OK
mytoken_with-alloc.release.polkavm 3980 3979 -0.03% OK
---------------------------------------------------- ------------ ------------ ---------- --------
Total 927958 928016 +0.01%

OK: All artifacts within 5% regression threshold

Run 28874914746 | 65e89f5 | 2026-07-07 15:01 UTC

@filip-parity filip-parity force-pushed the feat/storage-slot-eip1967 branch from 5baa149 to f0e96d7 Compare June 29, 2026 09:28

@re-gius re-gius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Just add a few fixes to the docs (CLAUDE.md)

get_slot bypasses the view/mut gate (a &self method can fabricate a writable handle without unsafe), so please add it to the "Honest caveat" escape-hatch list at CLAUDE.md:143 alongside raw uAPI and soften the CLAUDE.md:141 claim that #![forbid(unsafe_code)] closes that bypass

@smiasojed

Copy link
Copy Markdown
Collaborator

I think this EIP-1967 implementation belongs at the contracts level, not in the SDK - the same separation Stylus uses. In the Stylus ecosystem, stylus-sdk exposes only the low-level storage primitive (StorageType::new(slot, offset, host)), while the EIP-1967 constants and the StorageSlot helper live in the OpenZeppelin contracts library (rust-contracts-stylus).

Probably we should follow the same line: keep only the generic mechanism in the SDK - e.g. a #[slot(raw = …)] attribute (and the underlying StorageComponent::new_at_raw) that lets a contract bind a typed field to a fixed, externally-known slot while preserving the borrow-checker's view-vs-mutating gate. The EIP-1967 constant and any proxy logic then live in an example or a future contracts crate.
WDYT?

…nal slot (e.g. EIP-1967) while preserving the view/mut gate
@filip-parity filip-parity force-pushed the feat/storage-slot-eip1967 branch from f0e96d7 to e810d53 Compare July 7, 2026 14:41
@filip-parity filip-parity changed the title Add StorageSlot::get_slot typed accessor for fixed-key (EIP-1967) proxy storage Add #[slot(raw = KEY)] for binding typed storage fields to fixed external (EIP-1967) slots Jul 7, 2026
@filip-parity

Copy link
Copy Markdown
Contributor Author

@smiasojed reworked to follow the Stylus/OZ split you described. #[slot(raw = KEY)] is now the generic SDK mechanism: it binds a typed field to a fixed external slot through a real struct field, so get/set keep the &self/&mut self gate — no more writable handle from a view method.

One simplification vs your sketch: since main's StorageComponent::new_at already takes a StorageKey, I didn't need a separate new_at_raw — the macro just emits StorageKey::from_raw(KEY) with a right-aligned (32 - PACKED_BYTES) offset for sub-word types like Lazy<Address>. So it's macro-only, zero new pvm-storage surface. The EIP-1967 constant now lives in the example proxy, not the SDK.

@smiasojed

Copy link
Copy Markdown
Collaborator

@smiasojed reworked to follow the Stylus/OZ split you described. #[slot(raw = KEY)] is now the generic SDK mechanism: it binds a typed field to a fixed external slot through a real struct field, so get/set keep the &self/&mut self gate — no more writable handle from a view method.

One simplification vs your sketch: since main's StorageComponent::new_at already takes a StorageKey, I didn't need a separate new_at_raw — the macro just emits StorageKey::from_raw(KEY) with a right-aligned (32 - PACKED_BYTES) offset for sub-word types like Lazy<Address>. So it's macro-only, zero new pvm-storage surface. The EIP-1967 constant now lives in the example proxy, not the SDK.

Thanks, looks good - just one thing:
Using #[slot(raw = KEY)] on a single storage field forces every other field in the struct to be explicitly annotated with #[slot(N)], because the macro's mixing rule ) treats a raw field as "explicit" and forbids mixing explicit with auto-numbered fields. Since explicit #[slot(N)] is full-slot-only, this disables solc-style sub-word packing — fields that would have shared a slot now each consume a full 32-byte slot, so the contract's storage layout diverges from solc's. This could be improved because raw keys are pseudo-random and live outside the sequential range, so they can never collide with auto/numeric slots and shouldn't participate in the mixing rule at all. WDYT?

@smiasojed

Copy link
Copy Markdown
Collaborator

Please update the docs with this feature

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add StorageSlot::get_slot::<T>(SLOT_KEY) helper

3 participants