本文详细介绍了Cairo编程语言及其在StarkNet中的应用,强调了其与Solidity的相似性以及在区块链基础设施中的作用。作者分析了CairoContract的结构、数据类型、常见漏洞以及安全性问题,同时提供了一些实用的资源以帮助开发者提升编程能力和安全审计水平。
作者: Alexander Mazaletskiy
MixBytes 的安全研究员
Cairo 是一种编程语言,用于编写可证明的程序,其中一方可以向另一方证明某个计算已经正确执行。Cairo 和类似的证明系统可以用来为区块链提供可扩展性。
ZK-Rollup StarkNet 使用 Cairo 编程语言,既用于其基础设施,也用于编写 StarkNet 合约或其他 Cairo 合约。
Cairo 合约与 Solidity 合约类似。它们是有状态的,可以部署和交互,并存在于连接在一起的区块中。Cairo 合约被贴回到以太坊作为聚合 STARK 证明,以总结关键状态更新。证明使状态数据在以太坊上可用,且 Solidity 合约可以验证证明以获取 StarkNet 状态。StarkNet 是 L2,因为它继承了以太坊的安全性。
Cairo 合约代码与 Solidity/Vyper 非常相似,它包含结构、存储变量、事件、视图和非视图函等。
@event - 表示一个事件
@storage_var - 表示合约状态变量(即映射、地址等)
@constructor - 部署时运行一次的构造函数
@view - 用于读取存储变量
@external - 用于写入存储变量
@l1_handler - 用于处理从 L1 合约发送的消息
@constructor - 是部署时运行一次的构造函数,用于执行某种操作
@view - 用于读取存储变量
@external - 用于写入存储变量
程序通常看起来像这样
## 1. 结构体
struct MyStruct:
member my_name : felt
member my_age : felt
end
----------------------------
## 2. 事件
## 我们可以把结构体传递给事件
func NewStruct(new_struct : MyStruct):
end
----------------------------
## 3. 存储
@storage_var
func My_Struct(struct_id : felt) -> (struct-i : MyStruct):
end
----------------------------
## 5. 存储获取器
@view
func my_struct(struct_id : felt) -> (my_info : MyStruct):
return My_Struct.read(struct_id)
end
----------------------------
## 7. 非常量函数
@external
func new_struct(id : felt, name : felt, age : felt):
let my_struct = MyStruct(name, age)
My_Struct.write(id, my_struct)
NewStruct.emit(my_struct)
return ()
end
与 Solidity/Vyper 的相似性以及更大的社区促成了 Cairo 和 StarkNet 生态系统的普及。
MakerDao、Aave 和 Argent X 已经在使用 StarkNet 和 Cairo 合约迁移他们的解决方案。
但 Cairo 并不像看起来那么简单。
尽管外表简单,但一切并不像一开始看起来那样。
让我们更详细地看一下数据类型。
Felt
Felt 代表字段元素,是 Cairo 中唯一的数据类型。简单来说,它是一个无符号整数,最多可以有 76 位小数,但也可以用来存储地址。
字符串
目前,Cairo 不支持字符串。然而,它支持长达 31 个字符的短字符串,但它们实际上是以 felt 存储的。
## = 448378203247
let hello_string = 'hello'
数组
在 Cairo 中使用数组时,你需要一个指针,指向数组的开头,使用 alloc 方法声明为 felt\*。
可以使用 assert(稍后会提到)和指针向数组添加新元素。请参见下面的示例:
%lang starknet
%builtins range_check
## 导入以使用 alloc
from starkware.cairo.common.alloc import alloc
## 返回 felt 的视图函数
@view
func array_demo(index : felt) -> (value : felt):
# 创建一个指向数组开头的指针。
let (my_array : felt*) = alloc()
# 将 3 设置为数组第一元素的值
assert [felt_array] = 3
# 将 15 设置为数组第二元素的值
assert [felt_array + 1] = 15
# 将索引 2 设置为值 33。
assert [felt_array + 2] = 33
assert [felt_array + 9] = 18
# 访问选定索引的列表。
let val = felt_array[index]
return (val)
end
如果我们尝试从无效索引的数组中读取值,程序将以以下错误失败:未知的地址内存单元值。
你可以将数组用作函数参数或返回值,但在声明时,你应指明两个参数——数组的长度和数组本身。命名约定也很重要,应为 my_array_name 和 my_array_name_len。
例如:
%lang starknet
%builtins pedersen range_check
from starkware.cairo.common.cairo_builtins import HashBuiltin
## 一个接收数组作为参数的函数,因此实际上接收数组长度和数组本身
@external
func array_play(array_param_len : felt, array_param : felt*) -> (res: felt):
# 读取数组的第一个元素
let first = array_param[0]
# 读取数组的最后一个元素
let last = array_param[array_param_len - 1]
let res = first + last
return (res)
end
如果你不遵循正确的命名约定,你将从编译器收到以下错误:数组参数 "array_param" 必须由一个名为 "array_param_len" 的类型为 felt 的长度参数前置。
结构体和映射
结构体与 Solidity 类似。我们只需用 struct 关键字定义它,并将其所有属性定义为成员:
## 账户结构
struct Account:
member isOpen: felt
member balance: felt
end
要在 Cairo 中创建映射,你必须定义类型并在键和值之间使用 -> 符号。例如:
## 名为 "accounts_storage" 的映射,存储每个用户以其地址为键的账户详情
@storage_var
func accounts_storage(address: felt) -> (account: Account):
end
我们还可以从 Cairo 函数返回结构体。这种数据类型的限制引出了有趣的情况。
让我们看一下 Cairo 合约中的一些漏洞。
整数除法
是的,整数除法!
与 Solidity 不同,在 Solidity 中,除法是按照值作为实数进行计算的,小数点后的任何数值都会被截断;在 Cairo 中,更直观地将除法视为乘法的逆运算。当一个数字整除另一个数字时,返回的结果是我们预期的值,例如 30/6=5。但如果我们尝试除以一般不太适合的数字,例如 30/9,结果可能有些令人惊讶,此情况下的结果是 1206167596222043737899107594365023368541035738443865566657697352045290673497。因为 120…97 * 9 = 30。
## 错误案例
@external
func bad_normalize_tokens{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (normalized_balance : felt):
let (user) = get_caller_address()
let (user_current_balance) = user_balances.read(user)
let (normalized_balance) = user_current_balance / 10**18
return (normalized_balance)
end
算术溢出
默认的原始类型 felt 或字段元素,在某种程度上像任何其他语言中的整数,但需要注意几个重要差异。有效 felt 值的范围为 (-P/2,P/2)。这里的 P 是 Cairo 使用的素数,目前是一个 252 位的数字。使用 felt 进行的算术不会自动检查溢出,如果不适当考虑,会导致意想不到的结果。而且由于值的范围涵盖负值和正值,两个正数相乘可能会得到负值,反之亦然,两个负数相乘并不总是得到正值。
视图函数中的状态修改
Cairo 提供了 @view 装饰器,以表示一个函数不应做状态修改。然而,编译器目前并不强制执行这一点。 :(
## 错误案例
@view
func bad_get_nonce{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (nonce : felt):
let (user) = get_caller_address()
let (nonce) = user_nonces.read(user)
user_nonces.write(user, nonce + 1)
return (nonce)
end
不正确的 Felt 比较
还有 felt :) 在 Cairo 中,对于小于或等于比较运算符,有两种内置方法:assert_le 和 assert_nn_le。assert_le 断言一个数字 a 小于或等于 b,无论 a 的大小,而 assert_nn_le 还额外断言 a 是非负的,即不大于或等于范围检查边界值 2^128。
访问控制和账户抽象
StarkNet 使用的账户抽象模型与 Solidity 开发者可能习惯的模型有一些重要区别。在 StarkNet 中没有 EOA 地址,只有合约地址。用户通常会部署一个合约,该合约负责认证他们并代表用户发起进一步的调用,而不是直接与合约交互。在最简单的情况下,该合约检查交易是否由预期的密钥签名,但它还可以表示一个多签名或 DAO,或者包含更复杂的逻辑,关于允许哪些类型的交易(例如,存款和取款可以由不同的合约处理,或者它可以阻止不盈利的交易)。
当然,也可以直接与合约交互。但从合约的角度来看,调用者的地址将是 0。由于 0 也是未初始化存储的默认值,因此有可能意外构造出访问控制检查,这些检查未能正确限制仅对意图用户的访问。
@external
func bad_claim_tokens{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}():
# 可能为零
let (user) = get_caller_address()
let (user_current_balance) = user_balances.read(sender_address)
user_balances.write(user_current_balance + 200)
return ()
end
L1 到 L2 地址转换
在 Starknet 中,地址的类型是 felt,而在 L1 中,地址的类型是 uint160。因此,为了在跨层消息传递期间传递地址类型,地址变量通常以 uint256 提供。然而,这可能导致 L1 上的地址映射到 L2 上的零地址(或意外地址)。
其他类型的漏洞
由于 Starknet 的架构特征,还会出现以下漏洞:
Cairo 相对年轻,类似于早期版本的 Solidity。为了编写更好的代码,你需要使用经过验证的解决方案:
Cairo 合约是由 Starknet 提出的相对年轻的技术,在 Solidity 和 Vyper 开发者中因其视觉简单性和活跃增长的社区而受到欢迎。但 Cairo 合约充满了严重的漏洞,需要深入了解 Cairo 以及对安全审核的严肃态度。
https://github.com/gakonst/awesome-starknet
https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/
https://github.com/aave-starknet-project/aave-starknet-bridge
https://github.com/OpenZeppelin/nile
https://blog.openzeppelin.com/getting-started-with-openzeppelin-contracts-for-cairo/
https://github.com/OpenZeppelin/cairo-contracts
https://www.starknet-ecosystem.com
https://learnblockchain.cn/article/12425
https://github.com/NethermindEth/warp
https://hackmd.io/@0xHyoga/BkKhLIMJi
https://starknet.io/faq/starknet-contract-and-cairo-programs/
https://chainstack.com/starknet-cairo-developer-introduction-part-2/
MixBytes 是一个专业的区块链审计和安全研究团队,专注于为 EVM 兼容和基于 Substrate 的项目提供全面的智能合约审计和技术咨询服务。请加入我们,关注最新的行业趋势和洞察,在 X 上保持更新。
- 原文链接: mixbytes.io/blog/cairo-c...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!