Skip to content

Commit c876e47

Browse files
committed
Add StorageSlot::get_slot typed accessor and eip1967::IMPLEMENTATION_SLOT for fixed-key proxy storage slots
1 parent e4d7f01 commit c876e47

4 files changed

Lines changed: 205 additions & 1 deletion

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#![cfg(not(feature = "abi-gen"))]
2+
//! Worked example: a minimal EIP-1967 upgradeable proxy.
3+
//!
4+
//! The implementation address lives at the standard pseudo-random slot
5+
//! `keccak256("eip1967.proxy.implementation") - 1`, deliberately outside the
6+
//! compiler-assigned `#[slot(N)]` range so it never collides with regular
7+
//! fields. The contract reaches it through the typed `StorageSlot::get_slot`
8+
//! helper — `&self` reads, `&mut self` writes — instead of hand-rolling a raw
9+
//! `host.get_storage` call plus manual `Address` decoding.
10+
11+
use pvm_contract_sdk::{Address, Host, MockHost, MockHostBuilder, eip1967};
12+
13+
#[allow(dead_code)]
14+
#[pvm_contract_sdk::contract]
15+
mod upgradeable_proxy {
16+
use super::*;
17+
use pvm_contract_sdk::StorageSlot;
18+
19+
pub struct UpgradeableProxy;
20+
21+
impl UpgradeableProxy {
22+
#[pvm_contract_sdk::constructor]
23+
pub fn new(&mut self) {}
24+
25+
/// Current implementation address, read from the EIP-1967 slot.
26+
#[pvm_contract_sdk::method]
27+
pub fn implementation(&self) -> Address {
28+
StorageSlot::get_slot::<Address>(self.host(), eip1967::IMPLEMENTATION_SLOT).get()
29+
}
30+
31+
/// Point the proxy at a new implementation contract.
32+
#[pvm_contract_sdk::method]
33+
pub fn upgrade_to(&mut self, new_implementation: Address) {
34+
StorageSlot::get_slot::<Address>(self.host(), eip1967::IMPLEMENTATION_SLOT)
35+
.set(&new_implementation);
36+
}
37+
}
38+
}
39+
40+
use upgradeable_proxy::UpgradeableProxy;
41+
42+
fn proxy() -> (UpgradeableProxy, MockHost) {
43+
let mock = MockHostBuilder::new().build();
44+
let contract = UpgradeableProxy {
45+
host: Host::from_dyn(::std::rc::Rc::new(mock.clone())),
46+
};
47+
(contract, mock)
48+
}
49+
50+
#[test]
51+
fn implementation_defaults_to_zero() {
52+
let (p, _) = proxy();
53+
assert_eq!(p.implementation(), Address::from([0u8; 20]));
54+
}
55+
56+
#[test]
57+
fn upgrade_to_then_implementation_roundtrips() {
58+
let (mut p, _) = proxy();
59+
let impl_addr = Address::from([0x42; 20]);
60+
p.upgrade_to(impl_addr);
61+
assert_eq!(p.implementation(), impl_addr);
62+
}
63+
64+
#[test]
65+
fn upgrade_to_writes_the_eip1967_slot() {
66+
let (mut p, mock) = proxy();
67+
let impl_addr = Address::from([0xCD; 20]);
68+
p.upgrade_to(impl_addr);
69+
70+
// Independently confirm the write landed at the EIP-1967 slot — not some
71+
// compiler-assigned slot — by reading the raw 32 bytes back. `address`
72+
// stores right-aligned in its 32-byte word, mirroring solc.
73+
let raw = mock
74+
.get_raw_storage(&eip1967::IMPLEMENTATION_SLOT)
75+
.expect("implementation slot was written");
76+
let mut expected = [0u8; 32];
77+
expected[12..].copy_from_slice(impl_addr.as_ref() as &[u8; 20]);
78+
assert_eq!(raw.as_slice(), &expected);
79+
}

crates/pvm-contract-sdk/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ pub use pvm_contract_core::call::{
131131
// `uint8[]`, a different on-chain layout). `StorageComponent` is the trait
132132
// typed storage helpers implement to participate in auto-numbered slot layout.
133133
pub use pvm_storage::{
134-
AsStorageKey, LayoutStep, Lazy, Mapping, Ref, RefMut, StorageComponent, StorageKey, layout_step,
134+
AsStorageKey, LayoutStep, Lazy, Mapping, Ref, RefMut, StorageComponent, StorageKey,
135+
StorageSlot, eip1967, layout_step,
135136
};
136137

137138
#[cfg(feature = "abi-gen")]

crates/pvm-storage/src/lib.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,70 @@ impl StorageKey {
272272
}
273273
}
274274

275+
// ---------------------------------------------------------------------------
276+
// StorageSlot — typed access at a fixed, externally-known 32-byte key
277+
// ---------------------------------------------------------------------------
278+
279+
/// Well-known EIP-1967 proxy storage slots.
280+
///
281+
/// EIP-1967 places proxy bookkeeping at pseudo-random 32-byte slots, chosen so
282+
/// they never collide with compiler-assigned (sequential) storage slots. Every
283+
/// EVM proxy on Ethereum uses these exact values, so a contract reading them
284+
/// here sees the same data a Solidity proxy would.
285+
pub mod eip1967 {
286+
/// `bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)`.
287+
///
288+
/// The slot holding a transparent/UUPS proxy's implementation address.
289+
pub const IMPLEMENTATION_SLOT: [u8; 32] = [
290+
0x36, 0x08, 0x94, 0xa1, 0x3b, 0xa1, 0xa3, 0x21, 0x06, 0x67, 0xc8, 0x28, 0x49, 0x2d, 0xb9,
291+
0x8d, 0xca, 0x3e, 0x20, 0x76, 0xcc, 0x37, 0x35, 0xa9, 0x20, 0xa3, 0xca, 0x50, 0x5d, 0x38,
292+
0x2b, 0xbc,
293+
];
294+
}
295+
296+
/// Typed accessor for storage at a caller-supplied, externally-known 32-byte
297+
/// key — e.g. EIP-1967 proxy slots ([`eip1967::IMPLEMENTATION_SLOT`]).
298+
///
299+
/// Such slots live at fixed pseudo-random positions outside the
300+
/// compiler-assigned `#[slot(N)]` range, so they can't be reached through the
301+
/// normal contract-struct storage fields. [`get_slot`](StorageSlot::get_slot)
302+
/// gives them the same typed ergonomics as a regular field, returning a
303+
/// [`Lazy<T>`] handle with `get`/`set`/`try_get`, instead of forcing a raw
304+
/// `host.get_storage` call plus manual decoding.
305+
///
306+
/// ```ignore
307+
/// use pvm_storage::{StorageSlot, eip1967};
308+
/// use pvm_contract_types::Address;
309+
///
310+
/// let impl_addr: Address =
311+
/// StorageSlot::get_slot::<Address>(self.host(), eip1967::IMPLEMENTATION_SLOT).get();
312+
/// ```
313+
pub struct StorageSlot;
314+
315+
impl StorageSlot {
316+
/// Bind a [`Lazy<T>`] handle to the raw 32-byte storage `key`.
317+
///
318+
/// This is the blessed safe wrapper for arbitrary-key access: it owns the
319+
/// `unsafe` that [`Lazy::new`] requires (the key is supplied verbatim, not
320+
/// derived from the macro layout). As with any raw-slot access, the
321+
/// type-level view-vs-mutating gate does not apply — a `&self` method can
322+
/// obtain a writable handle. The runtime STATICCALL backstop still rejects
323+
/// an out-of-context `SSTORE`, so this is an SDK-level safety contract, not
324+
/// a soundness hole. Multi-slot `T` is striped across consecutive slots
325+
/// starting at `key`, matching [`Lazy`]'s layout.
326+
pub fn get_slot<T: StorageEncode + StorageDecode>(host: &Host, key: [u8; 32]) -> Lazy<T> {
327+
// A value occupying its own slot is right-aligned, matching solc's
328+
// storage layout: a sub-word `T` (e.g. `Address`, `PACKED_BYTES == 20`)
329+
// sits in the low-order bytes at offset `32 - PACKED_BYTES`, so a slot
330+
// written by a Solidity proxy decodes identically here. Full-slot `T`
331+
// (`PACKED_BYTES == 32`) yields offset 0.
332+
let offset = (32 - T::PACKED_BYTES) as u8;
333+
// SAFETY: `get_slot` exists precisely to expose arbitrary-key storage
334+
// access; the caller-supplied key is the intended `StorageKey`.
335+
unsafe { Lazy::new(StorageKey::from_raw(key), offset, host.clone()) }
336+
}
337+
}
338+
275339
// ---------------------------------------------------------------------------
276340
// AsStorageKey
277341
// ---------------------------------------------------------------------------

crates/pvm-storage/src/tests.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1899,3 +1899,63 @@ fn lazy_string_native_layout_matches_solc_short() {
18991899
assert!(header[5..31].iter().all(|&b| b == 0));
19001900
assert_eq!(header[31], 5 * 2);
19011901
}
1902+
1903+
// --- StorageSlot (fixed-key proxy slots, EIP-1967) ---
1904+
1905+
#[test]
1906+
fn eip1967_implementation_slot_matches_derivation() {
1907+
// The const must equal keccak256("eip1967.proxy.implementation") - 1,
1908+
// the canonical EVM-wide value every Solidity proxy uses. Computing it
1909+
// here (rather than trusting the literal) proves the derivation, not a copy.
1910+
let host = h();
1911+
let mut hash = [0u8; 32];
1912+
host.hash_keccak_256(b"eip1967.proxy.implementation", &mut hash);
1913+
let expected = U256::from_be_bytes(hash) - U256::from(1u64);
1914+
assert_eq!(eip1967::IMPLEMENTATION_SLOT, expected.to_be_bytes::<32>());
1915+
}
1916+
1917+
#[test]
1918+
fn storage_slot_roundtrips_address() {
1919+
let host = h();
1920+
let addr = Address([0xAB; 20]);
1921+
let mut slot = StorageSlot::get_slot::<Address>(&host, eip1967::IMPLEMENTATION_SLOT);
1922+
slot.set(&addr);
1923+
assert_eq!(
1924+
StorageSlot::get_slot::<Address>(&host, eip1967::IMPLEMENTATION_SLOT).get(),
1925+
addr
1926+
);
1927+
}
1928+
1929+
#[test]
1930+
fn storage_slot_stores_address_right_aligned_like_solc() {
1931+
// EIP-1967 interop hinges on matching solc's storage layout: a standalone
1932+
// `address` lives in the low-order 20 bytes of its slot. A slot written by
1933+
// a Solidity proxy must decode here, so the raw bytes must be right-aligned.
1934+
let host = h();
1935+
let addr = Address([0xCD; 20]);
1936+
StorageSlot::get_slot::<Address>(&host, eip1967::IMPLEMENTATION_SLOT).set(&addr);
1937+
1938+
let mut raw = [0u8; 32];
1939+
host.get_storage_or_zero(
1940+
StorageFlags::empty(),
1941+
&eip1967::IMPLEMENTATION_SLOT,
1942+
&mut raw,
1943+
);
1944+
let mut expected = [0u8; 32];
1945+
expected[12..].copy_from_slice(&addr.0);
1946+
assert_eq!(raw, expected);
1947+
}
1948+
1949+
#[test]
1950+
fn storage_slot_targets_exact_key_not_slot_zero() {
1951+
let host = h();
1952+
let mut slot = StorageSlot::get_slot::<U256>(&host, eip1967::IMPLEMENTATION_SLOT);
1953+
slot.set(&U256::from(99u64));
1954+
// A compiler-assigned #[slot(0)] field must be untouched by the proxy slot.
1955+
let field = unsafe { Lazy::<U256>::new(StorageKey::from_slot(0), 0, host.clone()) };
1956+
assert_eq!(field.get(), U256::ZERO);
1957+
assert_eq!(
1958+
StorageSlot::get_slot::<U256>(&host, eip1967::IMPLEMENTATION_SLOT).get(),
1959+
U256::from(99u64)
1960+
);
1961+
}

0 commit comments

Comments
 (0)