StarkNet上的合约存储是用简单的键/值对来处理的。如果一个合约从多个库导入,而这些库碰巧共享一个存储变量名(例如balance),如果编译器没有捕捉到,这些变量很可能会发生冲突。在撰写本文时,最好的解决方案是在存储变量名称前加上库的名称或命名空间。
StarkNet上的合约存储是用简单的键/值对来处理的。根据StarkNet文档:
存储布局
合约存储是一种持久存储空间,我们可以在其中读、写、修改和持久化数据。存储是一个有 2²⁵¹ 个插槽的地图,其中每个插槽初始化都为 0。
存储基本功能
读取存储返回的基本函数
value
存储在key
-Let (value) = storage_read(key)
写入存储的基本函数将value写入key -
storage_write(key, value)
在合约中使用@storage_var修饰的存储变量稍微复杂一些。StarkNet编译器将它们的名称和值(在Cairo代码中)映射到由StarkNet自己的sn_kecak方法(按原样或通过嵌套映射的哈希链)生成的地址。然而,这里的重要结论是,存储变量被简单地视为哈希的键/值对。
OpenZeppelin是可扩展性模式的先驱,该模式包含了从经过多次测试的库中安全导入功能和状态的合约指南。合约的基本思想是导入和公开(使用@external和@view装饰器)想在库中使用的方法。例如,考虑一下流行的ERC20库。库中已经存在用于部署ERC20代币的所有方法和状态管理。用户只需要在合约中公开必要的方法。
库不公开其方法的原因是,不管是否导入,Cairo都会自动导出这些方法。这可能很危险。
这个模式有趣的问题是:如果库使用存储变量设置它们自己的状态,那么当一个合约从多个库导入时,会发生什么情况?这些库的存储变量名称相同吗?
让我们看看这两个库:
# contracts/library_a
%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin
@storage_var
func balance() -> (res : felt):
end
namespace LIBRARY_A:
func increase_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}(amount : felt):
let (res) = balance.read()
balance.write(res + amount)
return ()
end
func get_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}() -> (res : felt):
let (res) = balance.read()
return (res)
end
end
# contracts/library_b
%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin
@storage_var
func balance() -> (res : felt):
end
namespace LIBRARY_B:
func increase_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}(amount : felt):
let (res) = balance.read()
balance.write(res + amount)
return ()
end
func get_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}() -> (res : felt):
let (res) = balance.read()
return (res)
end
end
虽然示例方法共享相同的名称,但它们属于各自的命名空间,即LIBRARY_A。increase_balance LIBRARY_B_increase_balance。但是,两个名称空间都不包含storage 变量。
现在,让我们来看一个合约,它将从这些库导入并公开它们的方法。
# contracts/contract_c
%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin
from contracts.library_a import LIBRARY_A
from contracts.library_b import LIBRARY_B
@external
func increase_balance_a{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}(amount : felt):
LIBRARY_A.increase_balance(amount)
return ()
end
@external
func increase_balance_b{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}(amount : felt):
LIBRARY_B.increase_balance(amount)
return ()
end
@view
func get_balance_a{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}() -> (res : felt):
let (res) = LIBRARY_A.get_balance()
return (res)
end
@view
func get_balance_b{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}() -> (res : felt):
let (res) = LIBRARY_B.get_balance()
return (res)
end
注意,两个库的存储都没有显式导入到合约中,只有命名空间。现在,让我们测试contract_c的公开方法,看看库各自的存储发生了什么变化。
# tests/test_clash.py
import pytest
import asyncio
from starkware.starknet.testing.starknet import Starknet
@pytest.fixture(scope='module')
def event_loop():
return asyncio.new_event_loop()
@pytest.mark.asyncio
async def test_clash():
starknet = await Starknet.empty()
contract_c = await starknet.deploy("contracts/contract_c.cairo")
await contract_c.increase_balance_a(99).invoke()
await contract_c.increase_balance_b(100).invoke()
balance_a = await contract_c.get_balance_a().call()
print("Should be 99: ", balance_a.result.res)
balance_b = await contract_c.get_balance_b().call()
print("Should be 100: ", balance_b.result.res)
结果:
StarkNet编译器不区分这两个balance存储变量,尽管它们似乎“私下”属于各自的库。换句话说,编译器将两个balance
存储变量视为对同一个变量的引用。
但是,如果同名存储变量有所不同,则StarkNet编译器将会显示失败。这些差异包括变量名、返回值名和键的数量。下面是一个简单的例子来说明:
# library_a
...@storage_var
func balance() -> (res: felt):
end
...
-----------------------------------------# library_b
...@storage_var
func balance() -> (var: felt):
end
...
如果像上面那样修改library_b中的返回变量名,并尝试编译contract_c,编译将失败。任何这样的差异将返回这个AssertionError:
f'Found two versions of auto-generated file "{input_file.filename}":\n' AssertionError: Found two versions of auto-generated file "autogen/starknet/storage_var/balance/impl.cairo": ...
主要结论:如果一个合约从多个库导入,而这些库碰巧共享一个存储变量名(例如balance),如果编译器没有捕捉到,这些变量很可能会发生冲突。
在撰写本文时,最好的解决方案是在存储变量名称前加上库的名称或命名空间。例如:ERC20_balances
、ReentrancyGuard_start
和Ownable_owner
。
为了进行概念验证,让我们将示例库的存储变量更改为LIBRARY_A_balance和LIBRARY_B_balance。
# contracts/library_a
%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin
@storage_var
func LIBRARY_A_balance() -> (res : felt):
end
namespace LIBRARY_A:
func increase_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}(amount : felt):
let (res) = LIBRARY_A_balance.read()
LIBRARY_A_balance.write(res + amount)
return ()
end
func get_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}() -> (res : felt):
let (res) = LIBRARY_A_balance.read()
return (res)
end
end
# contracts/library_b
%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin
@storage_var
func LIBRARY_B_balance() -> (res : felt):
end
namespace LIBRARY_B:
func increase_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}(amount : felt):
let (res) = LIBRARY_B_balance.read()
LIBRARY_B_balance.write(res + amount)
return ()
end
func get_balance{
syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
range_check_ptr}() -> (res : felt):
let (res) = LIBRARY_B_balance.read()
return (res)
end
end
运行完全相同的测试后,结果如下:
考虑到StarkNet网络和Cairo编程语言是多么的新颖和尖端,随着现有模式和惯例的发展,以及新的模式和惯例的出现,最佳实践将不可避免地发生变化。同时,为存储变量添加前缀!
Source:https://medium.com/coinmonks/storage-variable-clashing-in-starknet-ce5f28e60886
ChinaDeFi - ChinaDeFi.com 是一个研究驱动的DeFi创新组织,同时我们也是区块链开发团队。每天从全球超过500个优质信息源的近900篇内容中,寻找思考更具深度、梳理更为系统的内容,以最快的速度同步到中国市场提供决策辅助材料。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!