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