Storage and Contracts
This chapter covers Psy's storage system and contract development, including automatic storage generation, storage references, and contract architecture.
Storage Architecture Overview
Psy provides a sophisticated storage system with the following characteristics:
- Slot-based Storage: Maximum 2^32 slots per contract, each slot is a Hash type (4 Felt values)
- User Isolation: Each user has completely separate storage space within each contract
- Sequential Layout: Data is laid out in slots according to struct field order
- No Dynamic Types: No dynamic arrays or mappings - all storage is statically sized
- Automatic Code Generation: Use
#[derive(Storage, StorageRef)]for automated storage management
On-Chain Storage Architecture
Psy's on-chain storage follows a hierarchical tree structure that enables scalable and isolated user state management:
Global User Tree
At the top level, there is a Global User Tree that stores information for all users in the system. Each user has a User Leaf in this tree:
#![allow(unused)] fn main() { pub struct PsyUserLeaf { pub public_key: Hash, // User's public key for authentication pub user_state_tree_root: Hash, // Root of this user's state tree pub balance: Felt, // User's native token balance pub nonce: Felt, // Transaction nonce for replay protection pub last_checkpoint_id: Felt, // Last checkpoint this user participated in pub event_index: Felt, // Index for event ordering pub user_id: Felt, // Unique identifier for this user } }
User Contract State Tree Structure
Each user has their own User Contract State Tree with the following hierarchy:
Global User Tree
├── User 1 Leaf
│ ├── public_key
│ ├── user_state_tree_root ──┐
│ ├── balance │
│ ├── nonce │
│ ├── last_checkpoint_id │
│ ├── event_index │
│ └── user_id │
├── User 2 Leaf │
└── ... │
│
User Contract State Tree (per user) ──┘
├── Contract 1 State (2^32 slots max)
│ ├── Slot 0: [Felt; 4]
│ ├── Slot 1: [Felt; 4]
│ └── ...
├── Contract 2 State (2^32 slots max)
│ ├── Slot 0: [Felt; 4]
│ ├── Slot 1: [Felt; 4]
│ └── ...
└── ...
Storage Isolation and Access
This architecture provides several key benefits:
#![allow(unused)] fn main() { // Example: Understanding storage isolation fn storage_isolation_example() { // Each user has completely isolated storage let user_a_id = 123; let user_b_id = 456; let contract_id = 789; // User A and User B each have their own user leaf in the Global User Tree // The user leaf contains user_state_tree_root pointing to their contract state tree // Even in the same contract, users have separate storage let user_a_balance = get_other_user_contract_state_hash_at( 32, // tree height user_a_id, // User A contract_id, // same contract 0 // balance slot )[0]; let user_b_balance = get_other_user_contract_state_hash_at( 32, // tree height user_b_id, // User B contract_id, // same contract 0 // balance slot - completely separate from User A )[0]; // user_a_balance and user_b_balance are independent } }
Storage Access Permissions
Read Access: You can read from any user's storage in any contract
Write Access: You can only write to your own storage
#![allow(unused)] fn main() { fn storage_access_permissions() { // Get current user's information let my_user_id = get_user_id(); let my_public_key = get_user_public_key_hash(); let my_nonce = get_last_nonce(); // ✅ READ: Can read from any user's storage let other_user_id = 456; let contract_id = get_contract_id(); let slot_index = 0; // Read from any user's contract storage let other_user_slot_data = get_other_user_contract_state_hash_at( 32, // contract_state_tree_height other_user_id, // any user contract_id, // any contract slot_index // any slot ); // Read from current user's storage let my_slot_data = get_state_hash_at(slot_index); // ✅ WRITE: Can only write to own storage let new_data = [1, 2, 3, 4]; cset_state_hash_at(slot_index, new_data); // Only writes to current user's storage // ❌ CANNOT: Write to other users' storage // There is no function like cset_other_user_state_hash_at() // The architecture prevents writing to other users' storage } }
Contract Storage within User Contract State Tree
Each user's contract state tree contains their storage for all contracts:
#![allow(unused)] fn main() { #[contract] #[derive(Storage, StorageRef)] pub struct TokenContract { pub balance: Felt, // Slot 0 in this user's contract state pub allowances: [Felt; 100], // Slots 1-100 in this user's contract state } fn contract_storage_in_user_tree() { let contract = TokenContractRef::new(ContractMetadata::current()); // This accesses: // Global User Tree -> Current User Leaf -> User Contract State Tree Root // -> This Contract's State -> Specific Slots contract.balance.set(100); // Reading from another user requires traversing their tree: // Global User Tree -> Other User Leaf -> Their User Contract State Tree Root // -> This Contract's State in Their Tree -> Specific Slots let other_contract = TokenContractRef::new(ContractMetadata::new( get_contract_id(), 456 // other user's ID )); let other_balance = other_contract.balance.get(); } }
Storage Tree Limits
Each level has specific capacity limits:
#![allow(unused)] fn main() { // System limits const MAX_USERS: u64 = 2^64; // Global User Tree capacity const MAX_CONTRACTS_PER_USER: u32 = 2^32; // User Contract State Tree capacity const MAX_SLOTS_PER_CONTRACT: u32 = 2^32; // Contract Storage capacity fn storage_limits_example() { // Each user can have up to 2^32 contracts // Each contract can have up to 2^32 slots // Each slot stores Hash = [Felt; 4] // Total storage per user: 2^32 contracts × 2^32 slots × 4 Felt = 2^66 Felt values // Global capacity: 2^64 users × 2^66 Felt per user = 2^130 total Felt values } }
Contract Interaction Pattern: "Read Others, Write Self"
Psy contracts follow a "read others, write self" interaction pattern. Users can read from other users' storage but can only write to their own storage. This enables secure cross-user interactions:
#![allow(unused)] fn main() { impl TokenContract { // Transfer tokens: "read others, write self" pattern pub fn transfer_tokens(recipient: Felt, amount: Felt) { let sender_id = get_user_id(); // Write to sender's own storage (current user) let sender_contract = TokenContractRef::new(ContractMetadata::current()); let sender_balance = sender_contract.balance.get(); assert(sender_balance >= amount, "insufficient balance"); sender_contract.balance.set(sender_balance - amount); // Write transfer record to sender's storage using recipient as key sender_contract.transfers_sent.index(recipient).set( sender_contract.transfers_sent.index(recipient).get() + amount ); } pub fn claim_tokens(sender: Felt) { let recipient_id = get_user_id(); // Read from sender's storage (read others) let sender_contract = TokenContractRef::new( ContractMetadata::new(get_contract_id(), sender) ); let amount_sent = sender_contract.transfers_sent.index(recipient_id).get(); // Write to recipient's own storage (write self) let recipient_contract = TokenContractRef::new(ContractMetadata::current()); let amount_claimed = recipient_contract.transfers_claimed.index(sender).get(); let claimable = amount_sent - amount_claimed; assert(claimable > 0, "no tokens to claim"); // Update recipient's own storage recipient_contract.transfers_claimed.index(sender).set(amount_sent); recipient_contract.balance.set( recipient_contract.balance.get() + claimable ); } } }
Cross-User Interaction Examples
#![allow(unused)] fn main() { #[contract] #[derive(Storage, StorageRef)] pub struct MessageContract { pub messages_sent: [Hash; 1000000], // Messages sent to others pub messages_received: [Hash; 1000000], // Messages received from others } impl MessageContract { // Send message: write to own storage with recipient as index pub fn send_message(recipient: Felt, message_hash: Hash) { let sender = MessageContractRef::new(ContractMetadata::current()); sender.messages_sent.index(recipient).set(message_hash); } // Read message: read from sender's storage, write to own pub fn receive_message(sender: Felt) -> Hash { // Read from sender's storage let sender_contract = MessageContractRef::new( ContractMetadata::new(get_contract_id(), sender) ); let message = sender_contract.messages_sent.index(get_user_id()).get(); // Write to own storage to mark as received let recipient = MessageContractRef::new(ContractMetadata::current()); recipient.messages_received.index(sender).set(message); message } } }
Interaction Security Model
This pattern provides several security benefits:
- Write Isolation: Users can only modify their own storage
- Read Transparency: Users can read from any user's storage
- Consent-based Transfers: Recipients must actively claim transfers
- Audit Trail: All interactions are recorded in sender's storage
#![allow(unused)] fn main() { impl SecureContract { pub fn secure_interaction_example() { // ✅ Allowed: Read from any user's storage let other_user_data = OtherContractRef::new( ContractMetadata::new(get_contract_id(), 456) ).any_field.get(); // ✅ Allowed: Read from any user's any contract let external_data = ExternalContractRef::new( ContractMetadata::new(789, 456) // different contract, different user ).some_field.get(); // ✅ Allowed: Write to own storage only let my_contract = MyContractRef::new(ContractMetadata::current()); my_contract.my_data.set(42); // ❌ NOT Possible: Cannot write to another user's storage // The system architecture prevents this: // - No cset_other_user_state_hash_at() function exists // - Storage references only write to current user's storage // - Cross-user writes are architecturally impossible } } }
State Tree Operations
Understanding the tree structure helps optimize storage operations:
#![allow(unused)] fn main() { impl OptimizedContract { // Batch operations within same user are efficient pub fn batch_user_operations() { let contract = OptimizedContractRef::new(ContractMetadata::current()); // All these operations work within the same user contract state tree for i in 0u32..10u32 { contract.data.index(i as Felt).set(i as Felt); } // Efficient: single user contract tree, multiple contract slots } // Cross-user operations require multiple tree accesses pub fn cross_user_operation(other_user: Felt) { let my_contract = OptimizedContractRef::new(ContractMetadata::current()); let other_contract = OptimizedContractRef::new( ContractMetadata::new(get_contract_id(), other_user) ); // Less efficient: requires accessing two different user contract state trees let my_value = my_contract.data.index(0).get(); let other_value = other_contract.data.index(0).get(); } } ## Storage Capacity ```rust // Each contract supports up to 2^32 storage slots // Each slot is Hash type = [Felt; 4] = 4 u64 values const MAX_SLOTS: u32 = 2^32; type StorageSlot = Hash; // [Felt; 4] }
Storage Layout
Storage fields are arranged sequentially in the order they appear in the struct:
#![allow(unused)] fn main() { #[derive(Storage)] struct ExampleContract { pub field_a: Felt, // Slot 0 pub field_b: [Felt; 3], // Slots 1-3 pub field_c: CustomType, // Slots 4-6 (assuming CustomType::size() == 3) pub field_d: bool, // Slot 7 } // Total storage: 8 slots (0-7) }
Manual Storage Operations
Developers can manually manage storage slots using low-level functions:
Direct Slot Access
#![allow(unused)] fn main() { #[contract] struct TokenContract { } impl TokenContract { pub fn manual_storage_example() { let user_id = get_user_id(); // Read current user's balance from slot 0 // Each slot is a Hash containing [balance, reserved1, reserved2, reserved3] let user_leaf: Hash = get_state_hash_at(user_id); let current_balance: Felt = user_leaf[0]; // Update balance while preserving other fields let new_balance = current_balance + 100; cset_state_hash_at(user_id, [ new_balance, // New balance user_leaf[1], // Preserve reserved1 user_leaf[2], // Preserve reserved2 user_leaf[3], // Preserve reserved3 ]); // Access another user's storage let other_user = 456; let other_leaf: Hash = get_state_hash_at(other_user); let other_balance: Felt = other_leaf[0]; } pub fn cross_user_access() { let sender = 123; let recipient = get_user_id(); let contract_id = get_contract_id(); // Read sender's data from their storage in this contract let sender_data: Hash = get_other_user_contract_state_hash_at( 32, // contract_state_tree_height sender, // user_id of sender contract_id, // current contract recipient // slot index (using recipient as slot key) ); // sender_data[2] might contain amount sent to recipient let amount_sent = sender_data[2]; } } }
Manual Storage Layout Design
When using manual storage, developers design their own slot layout:
#![allow(unused)] fn main() { // Example: Token contract with manual slot management // Slot layout per user: // [balance, total_sent, total_received, reserved] impl TokenContract { pub fn transfer(recipient: Felt, amount: Felt) -> Felt { let sender_id = get_user_id(); // Read sender's state let sender_leaf: Hash = get_state_hash_at(sender_id); let sender_balance = sender_leaf[0]; let sender_total_sent = sender_leaf[1]; assert(amount <= sender_balance, "insufficient balance"); // Update sender's state cset_state_hash_at(sender_id, [ sender_balance - amount, // New balance sender_total_sent + amount, // Updated total sent sender_leaf[2], // Preserve total received sender_leaf[3], // Preserve reserved ]); // Read recipient's transfer tracking (from sender's perspective) let transfer_leaf: Hash = get_state_hash_at(recipient); let previous_sent_to_recipient = transfer_leaf[2]; // Update transfer tracking cset_state_hash_at(recipient, [ transfer_leaf[0], // Preserve balance transfer_leaf[1], // Preserve total sent previous_sent_to_recipient + amount, // Update sent to this recipient transfer_leaf[3], // Preserve reserved ]); sender_balance - amount } } }
Automatic Storage Generation
Use #[derive(Storage, StorageRef)] to automatically generate storage management code:
Storage Derive
The #[derive(Storage)] attribute automatically implements the Storage trait:
#![allow(unused)] fn main() { #[derive(Storage)] pub struct Person { pub age: Felt, // Size: 1 slot pub height: Felt, // Size: 1 slot pub birth_year: Felt, // Size: 1 slot } // Total size: 3 slots #[derive(Storage)] pub struct PersonArray { pub people: [Person; 10], // Size: 30 slots (10 * 3) pub count: Felt, // Size: 1 slot } // Total size: 31 slots }
Generated methods by #[derive(Storage)]:
size() -> Felt- Returns number of slots occupiedread(height: Felt, user_id: Felt, contract_id: Felt, offset: Felt) -> Selfwrite(offset: Felt, value: Self)
The #[derive(Storage)] attribute automatically implements the Storage trait by:
- Calculating size(): Sums up sizes of all fields
- Generating read(): Aggregates individual field reads from their slot offsets
- Generating write(): Aggregates individual field writes to their slot offsets
- Computing offsets: Each field gets an offset based on its position in the layout
#![allow(unused)] fn main() { // Example of what #[derive(Storage)] generates internally #[derive(Storage)] pub struct Person { pub age: Felt, // Offset 0, Size 1 pub height: Felt, // Offset 1, Size 1 pub birth_year: Felt, // Offset 2, Size 1 } // Total size: 3 slots // Generated implementation (simplified): impl Storage for Person { pub fn size() -> Felt { 3 // age(1) + height(1) + birth_year(1) } pub fn read(height: Felt, user_id: Felt, contract_id: Felt, offset: Felt) -> Self { // Read from slot offsets: offset+0, offset+1, offset+2 let age = Felt::read(height, user_id, contract_id, offset + 0); let height_val = Felt::read(height, user_id, contract_id, offset + 1); let birth_year = Felt::read(height, user_id, contract_id, offset + 2); new Person { age, height: height_val, birth_year } } pub fn write(offset: Felt, value: Self) { // Write to slot offsets: offset+0, offset+1, offset+2 Felt::write(offset + 0, value.age); Felt::write(offset + 1, value.height); Felt::write(offset + 2, value.birth_year); } } ## StorageRef Derive The `#[derive(StorageRef)]` attribute generates xxxRef types that provide `get` and `set` helper methods: ```rust #[derive(Storage, StorageRef)] pub struct TokenData { pub balance: Felt, // Offset 0, Size 1 pub locked_amount: Felt, // Offset 1, Size 1 } // Total size: 2 slots #[contract] #[derive(Storage, StorageRef)] pub struct TokenContract { pub total_supply: Felt, // Offset 0, Size 1 pub user_data: [TokenData; 1000000], // Offset 1, Size 2000000 (1M * 2) pub admin: Felt, // Offset 2000001, Size 1 } // Total size: 2000002 slots // Automatically generates TokenContractRef struct: pub struct TokenContractRef { pub total_supply: StorageRef<Felt, 1u32>, pub user_data: StorageRef<[TokenData; 1000000], 1u32>, pub admin: StorageRef<Felt, 1u32>, } impl TokenContractRef { // Generated constructor pub fn new(metadata: ContractMetadata) -> Self { new TokenContractRef { total_supply: StorageRef::<Felt, 1u32>::new(0, metadata), // Offset 0 user_data: StorageRef::<[TokenData; 1000000], 1u32>::new(1, metadata), // Offset 1 admin: StorageRef::<Felt, 1u32>::new(2000001, metadata), // Offset 2000001 } } } }
Generated xxxRef struct features:
- Virtual pointer: No data loaded until
get()is called - Automatic offsets: Each field reference points to correct slot offset
- Field access: Direct access to nested structure fields
- Array indexing:
index(i)method for array elements - get()/set() helper methods: Read and write individual values
Using Storage References
#![allow(unused)] fn main() { impl TokenContractRef { pub fn mint(user_id: Felt, amount: Felt) { // Create storage reference for current contract let contract = TokenContractRef::new(ContractMetadata::current()); // Access and modify total supply let current_supply = contract.total_supply.get(); contract.total_supply.set(current_supply + amount); // Access specific user's data through array indexing let mut user_data = contract.user_data.index(user_id).get(); contract.user_data.index(user_id).set(new TokenData { balance: user_data.balance + amount, locked_amount: user_data.locked_amount }); } pub fn transfer(from: Felt, to: Felt, amount: Felt) { let contract = TokenContractRef::new(ContractMetadata::current()); // Access sender's data let mut sender_data = contract.user_data.index(from).get(); assert(sender_data.balance >= amount, "insufficient balance"); // Update sender's balance contract.user_data.index(from).set(new TokenData { balance: sender_data.balance - amount, locked_amount: sender_data.locked_amount }); // Update recipient's balance let mut recipient_data = contract.user_data.index(to).get(); contract.user_data.index(to).set(new TokenData { balance: recipient_data.balance + amount, locked_amount: recipient_data.locked_amount }); } } }
Cross-User Storage Access
Storage references can access other users' storage:
#![allow(unused)] fn main() { pub fn claim_tokens(sender: Felt) { let current_user = get_user_id(); let contract_id = get_contract_id(); // Create reference to current user's contract storage let my_contract = ContractRef::new(ContractMetadata::current()); // Create reference to sender's contract storage let sender_metadata = ContractMetadata::new(contract_id, sender); let sender_contract = ContractRef::new(sender_metadata); // Read how much sender sent to current user let amount_sent = sender_contract.user_data.index(current_user).get().balance; // Read how much current user has already claimed from sender let amount_claimed = my_contract.user_data.index(sender).get().locked_amount; let claimable = amount_sent - amount_claimed; assert(claimable > 0, "nothing to claim"); // Update claimed amount and balance let mut my_data = my_contract.user_data.index(sender).get(); my_contract.user_data.index(sender).set(new TokenData { balance: my_data.balance, locked_amount: amount_sent // Update claimed amount }); my_contract.balance.set(my_contract.balance.get() + claimable); } }
Creating Storage Pointers
Storage references are virtual pointers that can be created without loading actual data:
#![allow(unused)] fn main() { fn storage_pointer_example() { // Create storage pointer for current user's contract let contract = ContractRef::new(ContractMetadata::current()); // Create pointer for different user's storage let other_user_contract = ContractRef::new(ContractMetadata::new( get_contract_id(), // same contract 456 // different user )); // Create pointer for different contract and user let external_contract = ContractRef::new(ContractMetadata::new( 789, // different contract ID 123 // different user ID )); // All pointers are lightweight - no data loaded until get() is called let my_balance = contract.balance.get(); // Load from current user let other_balance = other_user_contract.balance.get(); // Load from user 456 let external_balance = external_contract.balance.get(); // Load from user 123 in contract 789 } }
Contract Definition
Contract Attributes
#![allow(unused)] fn main() { // Basic contract struct #[contract] #[derive(Storage, StorageRef)] pub struct MyContract { pub state_var1: Felt, pub state_var2: [Felt; 100], } // Alternative: storage-only struct (no automatic contract generation) #[storage] #[derive(Storage)] pub struct DataStorage { pub data: [Felt; 1000], } }
Contract Implementation
#![allow(unused)] fn main() { impl MyContract { // Constructor pattern pub fn new() -> Self { new MyContract { state_var1: 0, state_var2: [0; 100], } } // Generated getter/setter methods (when using #[derive(Storage)]) pub fn get_state_var1(height: Felt, user_id: Felt, contract_id: Felt) -> Felt { // Auto-generated } pub fn set_state_var1(value: Felt) { // Auto-generated - writes to current user's storage } } impl MyContractRef { // Storage reference methods pub fn initialize() { let contract = MyContractRef::new(ContractMetadata::current()); contract.state_var1.set(42); // Initialize array elements for i in 0u32..100u32 { contract.state_var2.index(i as Felt).set(i as Felt); } } } }
Nested Structures and References
Nested Structure Access
#![allow(unused)] fn main() { #[derive(Storage, StorageRef)] pub struct UserProfile { pub name_hash: Hash, pub age: Felt, pub balance: Felt, } #[derive(Storage, StorageRef)] pub struct GameData { pub level: Felt, pub score: Felt, } #[contract] #[derive(Storage, StorageRef)] pub struct UserContract { pub profile: UserProfile, // Slots 0-5 (Hash=4 + Felt + Felt) pub game: GameData, // Slots 6-7 pub friends: [Felt; 10], // Slots 8-17 } impl UserContractRef { pub fn update_profile_age(new_age: Felt) { let user = UserContractRef::new(ContractMetadata::current()); // Update profile age let mut profile = user.profile.get(); profile.age = new_age; user.profile.set(profile); // Access nested fields let mut current_profile = user.profile.get(); current_profile.balance = current_profile.balance + 10; user.profile.set(current_profile); // Array access user.friends.index(0).set(123); // Set first friend ID } } }
Ref Attribute for Nested Data Access
Use #[ref] to create references for nested data:
#![allow(unused)] fn main() { #[derive(Storage, StorageRef)] pub struct NestedData { pub counter: Felt, pub last_update: Felt, } #[contract] #[derive(Storage, StorageRef)] pub struct MyContract { pub basic_data: Felt, #[ref] pub nested_data: NestedData, pub array_data: [Felt; 1000], } impl MyContractRef { pub fn increment_counter() { let contract = MyContractRef::new(ContractMetadata::current()); // Access nested data through #[ref] let mut data = contract.nested_data.get(); data.counter = data.counter + 1; data.last_update = get_checkpoint_id(); contract.nested_data.set(data); } } }
Storage Size Calculation
#![allow(unused)] fn main() { #[test] fn test_storage_sizes() { // Primitive types assert_eq(Felt::size(), 1, "Felt occupies 1 slot"); assert_eq(bool::size(), 1, "bool occupies 1 slot"); assert_eq(Hash::size(), 4, "Hash occupies 4 slots"); // Arrays assert_eq(<[Felt; 10]>::size(), 10, "Array size = element_count"); assert_eq(<[Hash; 5]>::size(), 20, "Hash array: 5 * 4 = 20 slots"); // Custom structures // Person has 3 Felt fields = 3 slots assert_eq(Person::size(), 3, "Person occupies 3 slots"); // Contract with mixed types assert_eq(UserContract::size(), 18, "Total contract storage"); } }
Storage Best Practices
Layout Organization
#![allow(unused)] fn main() { // Good: Related data together #[derive(Storage, StorageRef)] pub struct WellDesignedContract { pub balance: Felt, pub last_active: Felt, pub user_level: Felt, pub experience: Felt, pub historical_data: [Felt; 1000], } }
Choosing Manual vs Automatic Storage
Use Manual Storage When:
- Need precise control over slot layout
- Implementing complex storage patterns
- Working with legacy storage layouts
- Optimizing for specific access patterns
#![allow(unused)] fn main() { // Manual storage for precise control impl AdvancedTokenContract { // Custom layout: [balance, allowance, metadata, reserved] pub fn get_user_balance(user_id: Felt) -> Felt { let user_leaf = get_state_hash_at(user_id); user_leaf[0] // Balance is always first element } } }
Use Automatic Storage When:
- Developing new contracts
- Need clean, maintainable code
- Working with structured data
- Want type safety and error prevention
#![allow(unused)] fn main() { // Automatic storage for clean code #[derive(Storage, StorageRef)] pub struct CleanTokenContract { pub balances: [Felt; 1000000], pub allowances: [Hash; 1000000], // [owner, spender, amount, expiry] pub metadata: TokenMetadata, } }
Key Points
- Hierarchical Storage: Global User Tree → User Contract State Tree → Contract Storage (2^32 slots)
- User Leaf Structure: Each user has a leaf containing public key, contract state tree root, balance, nonce, etc.
- Complete Isolation: Each user has separate contract state trees - no cross-contamination
- Storage Capacity: 2^64 users × 2^32 contracts × 2^32 slots × 4 Felt per slot
- Access Permissions: Read any user's storage, write only to own storage
- Interaction Pattern: "Read Others, Write Self" - enables secure cross-user interactions
- Security Model: Write isolation ensures users cannot modify others' storage directly
- Consent-based Transfers: Recipients must actively claim transfers from senders
- Two Approaches: Manual slot management vs automatic Storage/StorageRef derives
- Storage References: Virtual pointers enable efficient access without full data loading
- Cross-User Access: Create ContractMetadata with different user_id for cross-user reads
- Type Safety: Automatic derives provide compile-time layout validation
- No Dynamic Types: All storage must be statically sized at compile time
- Storage Pointers: Use
ContractRef::new(ContractMetadata)to create lightweight virtual pointers - Generated Helper Methods:
#[derive(StorageRef)]generates xxxRef types withgetandsetmethods