Web3.0 安全开发实践:Clarity 最佳实践总结
本文将探讨 Clarity 智能合约背后的概念,以及使用 Clarity 编程的最佳实践和安全检查清单。
在过去的一段时间里,CertiK 团队对比特币生态系统及其发展进行了深入研究。同时,团队还审计了多个比特币项目以及基于不同编程语言的智能合约,包括 OKX 的 BRC-20 钱包和 MVC DAO 的 sCrypt 智能合约实现。
现在,我们的研究重点转向了 Clarity。CertiK 团队在圆满完成多个 Clarity 漏洞赏金项目后,获得了更多关于其安全问题和常见实践的洞见。本文将分享这些见解以及经验,以期可以帮助到生态建设者。
Clarity 是一种由 Hiro PBC、Algorand 和其他利益相关者共同开发的智能合约语言。目前,它已在 Stacks 链(比特币侧链)上得到应用。Clarity 的主要目标是提供高度的可预测性和安全性,确保智能合约按预期执行,不会产生任何出乎意料的副作用。
接下来,我们将探讨 Clarity 智能合约背后的概念,以及使用 Clarity 编程的最佳实践和安全检查清单。
Clarity 语言
Clarity 的设计源自对智能合约工程中漏洞的深入分析,特别是对 Solidity 中观察到的漏洞的研究。它的关键特性包括以下几点:
- 可解释语言:确保所见即所得。
- 可判定属性:保证可预测结果和有限执行。
- 安全措施:防范重入攻击、溢出和下溢,这些对保持合约完整性至关重要。
- 自定义代币支持:简化开发流程,便于创建和管理代币。
- 事务后置条件:通过验证执行后的状态变化来增强安全性。
Clarity 的独特之处在于它从 LISP 语言中汲取灵感,LISP 以其处理符号信息的简洁性和强大功能而闻名。在 Clarity 中,一切都以“列表中的列表”或“表达式中的表达式”的形式表示。这种嵌套结构是 Clarity 的核心特点,使其语言具有高度的表达性和灵活性。函数定义、变量声明和函数参数都被封装在括号内,强调了语言的语法统一性。
以下是定义一个简单 Clarity 函数的示例:
(define-data-var count int 0) //State Variable Declaration(define-public (increase-number (number int)) //Function definition (let ( (current-count count) ) (var-set count (+ 1 number)) (ok (var-get count)) ))(increase-number 1) //Function call通过理解和利用这些嵌套表达式,开发者可以创建符合 Stacks 区块链功能要求的安全高效的智能合约。这种方法不仅增强了可读性,还确保合约的确定性和可预测性——这是保持去中心化应用安全性和可信度的关键。
Clarity 与 Solidity 的区别
1. 解释型与编译型
Clarity:Clarity 是一种解释型语言,意味着源代码直接发送到 Stacks 区块链并在区块链上执行,而无须编译成字节码。
Solidity:Solidity 代码首先需要编译成字节码,然后将字节码部署到区块链上。EVM(以太坊虚拟机)会验证这些字节码,并调度相应的操作码进行执行。
2. 无动态调度
Clarity 不支持动态调度,并且不是图灵完备的,这意味着要执行的函数是预先确定的。这个特性简化了执行模型,并有助于防止重入攻击,使 Clarity 本身更加安全。
Clarity 智能合约安全
安全一直是 DeFi 领域的头等大事,尤其是在 Stacks 网络扮演关键角色的比特币 DeFi 生态系统中。截至 2024 年 8 月,Stacks 生态系统中的总锁仓价值(TVL)已达到约为 8000 万美元,因此强大的安全措施变得尤为重要。
截至目前,Stacks 已经发生了多起安全事件,导致超过 200 万美元的损失。这些事件突显了对 Clarity 智能合约进行安全审计的必要性。
安全事件案例
2024 年 4 月 11 日,Stacks 网络上的借贷协议Zest Protocol(比特币 L2)遭遇了一次重大漏洞攻击,攻击目标是协议的借款池(Borrow Pool),导致损失约 322,000STX(折约 100 万美元)。这次黑客攻击事件迄今为止是比特币 DeFi 生态系统中损失最严重的事件。
Zest Protocol 的借款功能在合约 pool-borrow.clar 中定义,允许用户通过提供抵押物来借入资产。该功能的参数包括池储备、价格预言机、借入的资产、流动性提供者代币、抵押资产列表、借款金额、费用计算器、利率模式和所有者:
(define-public (borrow (pool-reserve principal) (oracle <oracle-trait>) (asset-to-borrow <ft>) (lp <ft>) (assets (list 100 { asset: <ft>, lp-token: <ft>, oracle: <oracle-trait> })) (amount-to-be-borrowed uint) (fee-calculator principal) (interest-rate-mode uint) (owner principal))攻击者利用了 assets(资产)参数,该参数是一个最多包含 100 种资产的列表,用作抵押物。其漏洞源于合约未能成功验证抵押资产的唯一性。更具体地说,合约在验证资产存在性时未检查重复项。这个疏忽使攻击者能够通过多次列出同一资产操纵抵押物的价值。
其他协议也遭遇过类似的安全漏洞攻击。例如,2021 年 10 月,一名攻击者从Arkadiko Swap中窃取了约 400,000 枚 STX 和 740,000 枚 USDA(总计约 150 万美元)。攻击者利用了 Arkadiko Swap 智能合约代码中的一个漏洞,该漏洞未能在创建新的交易对时正确验证 LP 代币。该漏洞使攻击者能够零成本铸造大量 LP 代币,随后从 STX/USDA 池中提取底层资产,影响了该池总价值的 25% 的资产。
Clarity 智能合约的最佳实践与检查清单
我们总结了广泛研究后取得的经验,并为 Clarity 智能合约开发者编制了最佳实践和检查清单。以下是关键点:
1. 避免使用 -panic 函数
在 Clarity 智能合约中解包值时,避免使用 unwrap-panic 和 unwrap-err-panic 等函数。当这些函数解包失败时,它们会以运行时错误中止调用,却也没有为与合约交互的应用程序提供有意义的信息。但如果选择使用 unwrap!和 unwrap-err!,并附加明确的错误代码。这个方法不仅能够提高了错误的处理能力,还便于调试,并增强了智能合约的韧性。使用具体的错误代码可以使调用应用程序更高效地处理错误,并根据上下文采取适当的措施。
2. 避免使用 tx-sender 进行验证
在 Clarity 智能合约中滥用 tx-sender 变量进行身份验证可能导致安全漏洞,类似于 Solidity 中SWC-115列出的漏洞。tx-sender 变量标识调用链的发起者,类似于 Solidity 中的 tx.origin。使用 tx-sender 进行验证可能会引发网络钓鱼攻击,攻击者可以欺骗用户并在易受攻击的合约上执行经过身份验证的操作。
tx-sender 与 contract-caller 的对比
另一方面,contract-caller 表示当前调用的发送者。通过避免使用 tx-sender 进行身份验证,并采用更安全的替代方案如 contract-caller,开发者可以降低网络钓鱼攻击和跨站脚本攻击的风险。
3. 模块化合约设计以增强灵活性和未来可升级性。
一旦智能合约部署到区块链上,它就变得不可修改。与传统应用开发相比,这种不可变性带来了挑战,这意味着它不可以随时进行更新和修复。在智能合约开发中,确保灵活性和未来可升级性需要采取战略方法,因为一旦合约部署,就没有直接的方法可以更新合约代码。
解决这些挑战,开发者应考虑以下原则:
- 保持逻辑分离:避免创建一个处理所有功能的单一合约。而应该,通过将智能合约模块化,将其拆分为更小、独立的并且可以相互交互的组件。这种方法不仅使合约更易于管理和理解,还能在不影响整个系统的情况下替换或升级单个组件。
- 无状态合约:该合约可以在区块链上存储最少量的数据,从而减少了未来更改的复杂性和潜在影响。通过将状态保留在合约外部并作为输入参数传递,您可以更新逻辑而无须修改合约的状态。
- 避免硬编码变量:将值直接硬编码到合约代码中可能导致其缺乏灵活性,且妨碍未来的更新。相反,若将关键变量定义为可配置的参数,这些参数则可以通过合约函数进行设置或调整。
4. 避免基于区块高度(block-hight)的时间计算
在 Clarity 智能合约中,避免依赖 block-height 关键字进行时间敏感的计算。Stacks 链的区块时间可能会随着网络升级而变化,例如Nakamoto 版本的发布将会减少区块时间。而应该使用 burn-block-height 关键字,它反映了底层比特币区块链的当前区块高度。比特币的区块时间更稳定,不太可能发生变化,从而确保合约操作的更高准确性和可靠性。这种做法有助于保持一致性,并防止由于 Stacks 区块时间波动而引发的潜在问题。
5. 正确处理函数中的返回值
在开发 Clarity 智能合约时,必须正确处理函数的布尔返回值,尤其是处理像 verify-mined() 等函数时。
该函数返回三种可能的值:(ok true)、(ok false) 或错误。如果返回 (ok true),表示交易已在指定区块成功挖掘。如果返回 (ok false),则表示交易未被挖掘,而错误则表示 Merkle(梅克尔)证明存在问题。
在使用 try!检查 ok/error,但未能验证响应类型中封装的布尔值时,会出现一个常见问题。这种疏忽可能导致即使交易未在区块中被挖掘(即返回值为 (ok false)),函数也不会失败。结果是验证者可能会合作签署一个未被挖掘的交易,使其未经检查就通过索引器。这一漏洞使得未经验证的未挖掘和潜在恶意的交易得以处理,从而导致安全漏洞和系统内未经授权的操作。
为了降低这种风险,请确保您的代码检查错误并明确验证函数返回的布尔值。这种做法有助于维护合约的完整性和安全性,确保只处理有效的挖矿交易。
6. 在 Clarity 中正确使用 contract-call?
在开发 Clarity 智能合约时,必须使用 contract-call?函数正确实现合约间调用。该函数从被调用的智能合约返回一个 Response(响应)类型的结果。
contract-call?的两种类型:
静态调用(Static Call):被调用的是一个已知的不变合约,在调用者合约部署时可在链上使用。第一个参数是被调用者的本金,然后是方法名称及其参数。
(contract-call? .registrar register-name name-to-register)动态调用(Dynamic Call):将被调用者作为参数传递,并将其类型化为特征引用(trait reference)。通过引用特征,代码可以更加灵活和可重用。
(define-public (swap (token-a <can-transfer-tokens>) (amount-a uint) (owner-a principal) (token-b <can-transfer-tokens>) (amount-b uint) (owner-b principal))) (begin (unwrap! (contract-call? token-a transfer-from? owner-a owner-b amount-a)) (unwrap! (contract-call? token-b transfer-from? owner-b owner-a amount-b))))在处理合约调用时,请注意以下限制:
- 在静态调用中,被调用者智能合约在创建时必须存在。
- 智能合约的调用图中不得存在循环。这可以防止递归(和重入)。这种结构可以通过对调用图的静态分析检测出来,并将被网络拒绝。
- contract-call? 仅用于合约间调用。当调用者同时也是被调用者时,如果尝试执行,则会中止交易。
结束
CertiK 已对 Clarity 智能合约安全进行了广泛地研究。作为一家在智能合约安全领域拥有丰富经验的 Web3.0 头部安全审计公司,CertiK 已发现并报告了多个基于 Clarity 的漏洞赏金项目中的漏洞。如需了解我们之前的风险分析报告,可以访问我们的博客。
免责声明:文章中的所有内容仅代表作者的观点,与本平台无关。用户不应以本文作为投资决策的参考。
你也可能喜欢
以军深入黎巴嫩南部,黎政府指责以违反停火协议800多次
以黎停火协议的60天执行期已经接近一半,尽管双方都互指对方违反协议,但由美国主导的国际监督机制尚未公开介入。
资深交易员的2024“修炼宝典”:如何从波动中捕捉机会,守住利润
本文PANews通过对多个资深交易员的采访与整理,分享他们在2024年中的操作经验与投资教训,探讨如何优化交易策略,捕捉市场机会并守住利润。