写入任何存储槽
在Starknet上,一个合约的存储是一个拥有 2^251 个槽的map,每个槽是一个初始化为 0 的 felt。存储变量的地址在编译时通过公式计算得出:存储变量地址 := pedersen(keccak(变量名), keys)
。与存储变量的交互通常使用 self.var.read()
和 self.var.write()
。
然而,我们可以使用 storage_write_syscall
和 storage_read_syscall
系统调用,来对任何存储槽进行写入和读取。
这在写入那些在编译时还未确定的存储变量时非常有用,这也可以确保即使合约升级且存储变量地址的计算方法改变,这些变量仍然可访问。
在以下示例中,我们使用 Poseidon 哈希函数来计算存储变量的地址。Poseidon 是一个 ZK 友好的哈希函数,比 Pedersen 更便宜、更快,是链上计算的绝佳选择。一旦地址被计算出来,我们就使用存储的系统调用与之交互。
#[starknet::interface]
trait IWriteToAnySlots<TContractState> {
fn write_slot(ref self: TContractState, value: u32);
fn read_slot(self: @TContractState) -> u32;
}
#[starknet::contract]
mod WriteToAnySlot {
use starknet::syscalls::{storage_read_syscall, storage_write_syscall};
use starknet::SyscallResultTrait;
use poseidon::poseidon_hash_span;
use starknet::storage_access::Felt252TryIntoStorageAddress;
use starknet::StorageAddress;
#[storage]
struct Storage {}
const SLOT_NAME: felt252 = 'test_slot';
#[abi(embed_v0)]
impl WriteToAnySlot of super::IWriteToAnySlots<ContractState> {
fn write_slot(ref self: ContractState, value: u32) {
storage_write_syscall(0, get_address_from_name(SLOT_NAME), value.into());
}
fn read_slot(self: @ContractState) -> u32 {
storage_read_syscall(0, get_address_from_name(SLOT_NAME))
.unwrap_syscall()
.try_into()
.unwrap()
}
}
fn get_address_from_name(variable_name: felt252) -> StorageAddress {
let mut data: Array<felt252> = ArrayTrait::new();
data.append(variable_name);
let hashed_name: felt252 = poseidon_hash_span(data.span());
let MASK_250: u256 = 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
// By taking the 250 least significant bits of the hash output, we get a valid 250bits storage address.
let result: felt252 = (hashed_name.into() & MASK_250).try_into().unwrap();
let result: StorageAddress = result.try_into().unwrap();
result
}
}