智能合约安全最佳实践
本指南对智能合约安全的最佳实践进行了实用而有条理的阐述。 智能合约是大多数链上应用的动力源。 它们定义并执行各种用例背后的逻辑,包括去中心化金融、玩赚游戏、现实世界资产代币化等。
在 Kaia 生态系统中,尤其是在开发 Mini dApps 的开发者中,这些应用程序的采用和使用率迅速上升。 然而,通过智能合约交易或锁定的价值越多,就越有可能吸引恶意行为者。 这些攻击者通常关注系统的核心--智能合约。
因此,智能合约的安全性绝不能事后才考虑。 从开发的最初阶段到部署以及与合同的持续互动,都应将其作为优先事项。
什么是智能合约安全?
智能合约是存储在区块链上的程序,在满足预定义条件时自动执行。 一旦部署,其代码就变得不可更改,即无法更改。 这种不变性确保了透明度,消除了对中介的需求,但也带来了严重的风险。 如果合同包含漏洞,在部署后就无法修补,这可能会导致资金被盗和失去信任。
智能合约安全是指用于保护这些合约免受恶意攻击和程序缺陷的一系列实践和措施。 一份安全可靠的合同有助于防止未经授权的访问、数据篡改和财务损失,从而保护协议的完整性。
智能合约安全为何重要?
由于智能合约在部署后不可更改,因此任何错误或漏洞都将成为永久性的。 恶意行为者可以利用这些弱点耗尽资金或操纵协议行为。 在许多情况下,代码中的一个错误就可能导致数百万美元的损失。
根据 DeFiLlama 的数据,截至 2025 年 6 月,在去中心化金融攻击中被盗的总金额高达 66 亿美元。 其中,智能合约漏洞利用约占 33 亿美元,约为 51%。 这些数据凸显了智能合约安全对于任何链上协议的重要性。
编写安全智能合约的最佳实践
1. 使用经过严格测试的安全库或函数
在智能合约中使用外部依赖项,如果这些依赖项没有经过适当测试或审查,就可能引入恶意代码。 为降低这种风险,应始终依赖经过严格测试且广受信任的库,如 OpenZeppelin,这些库由一个强大的社区维护,并定期接受审核。
此外,在将第三方代码纳入合同之前,要对其进行彻底审查。 测试和审查外部代码有助于确保其不包含隐藏的漏洞或在协议中引入意外行为。
2. 实施开发安全模式
安全模式是防御已知攻击载体(如重入性)的标准化技术。 它们提供了一种可靠且广为接受的方法,可在漏洞出现之前加以预防。 将这些模式融入代码可提高代码的弹性,降低漏洞利用的风险。 以下是一些需要考虑的基本安全模式:
2.1 CEI 模式(检查-影响-互动)
CEI 模式有助于确保在进行任何外部交互之前完成所有必要的验证。 这种结构降低了智能合约执行过程中出现意外或恶意行为的几率。
如果实施得当,CEI 模式会遵循以下顺序:
- 检查:确认满足所有必要条件(例如,确认用户有足够的余额)。
- 效果:更新合约的内部状态(如减少用户的余额)。
- 互动:转账或调用外部合同。
采用这种结构可以大大降低重入点攻击的风险。
例如,下面的示例就存在漏洞,因为它会在更新用户余额之前向其发送以太币。
contract InSecureBank { mapping(address => uint256) public balances; function deposit() public payable { require(msg.value > 0, "Deposit amount must be greater than zero"); balances[msg.sender] += msg.value; } function withdraw(uint256 amount) public { // Checks: if user have enough balance require(balances[msg.sender] >= amount, "Insufficient balance"); // observe that this is an this external interaction. // should be made after deducting the `amount` from the user's balance (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Withdrawal failed"); // Effects: Update the user's balance balances[msg.sender] -= amount; }}
上述代码容易受到重入攻击,恶意行为者可以在余额更新前重复调用提款函数。 CEI 模式可确保在进行任何外部调用之前更新合约状态,从而避免出现这种情况。
下面是上述代码的更新版本,它采用了 CEI 模式:
contract SecureBank { mapping(address => uint256) public balances; function deposit() public payable { require(msg.value > 0, "Deposit amount must be greater than zero"); balances[msg.sender] += msg.value; } function withdraw(uint256 amount) public { // Checks: Ensure that the user has enough balance to withdraw the requested amount require(balances[msg.sender] >= amount, "Insufficient balance"); // Effects: Update the user's balance balances[msg.sender] -= amount; // Interactions: Transfer the requested amount to the user (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Withdrawal failed"); }}
在遵循 CEI 模式的更新版本中,余额会在任何代币转移之前进行调整。 这样,即使用户试图重新输入功能,调用也会失败,因为他们的余额已经减少。
2.2. 紧急停止模式
紧急停止模式通常被称为断路器,允许在紧急情况下暂停智能合约中的选定功能。 如果检测到漏洞或意外行为,它提供了一种快速禁用关键操作的方法。
要主动监控您的合同是否存在此类问题,您可以使用监控工具或为此目的定制的机器人。 这些机器人会扫描特定的交易模式或合同状态的异常变化,以标记潜在威胁。
下面的示例演示了如何在合同中实施断路器机制:
contract CircuitBreaker { address public owner; bool public contractStopped = false; constructor(address _owner) { owner = _owner; } modifier onlyOwner() { require(owner == msg.sender, "Not the owner"); _; } // Only works when contract is running modifier haltInEmergency() { require(!contractStopped, "Contract is stopped"); _; } // Only works when contract is paused modifier enableInEmergency() { require(contractStopped, "Contract is running"); _; } // Owner can pause/unpause contract function toggleContractStopped() public onlyOwner { contractStopped = !contractStopped; } // Normal operations (when running) function deposit() public payable haltInEmergency { // Deposit logic here } // Emergency functions (when paused) function emergencyWithdrawal() public onlyOwner enableInEmergency { // Emergency withdrawal logic here }}
2.3. 减速带图案
在执行关键链上操作(如提款或治理决策)之前,"减速带 "模式会引入一段延迟时间。 这种延迟可作为一种保护措施,让用户或管理员有时间检测和应对可疑活动。
例如,您可以通过设置固定的等待期或最高取款金额来限制取款。 这有助于防止未经授权的访问或恶意行为导致的资金快速耗尽。
下面的示例展示了这种模式的工作原理,即在用户取款前强制执行五天等待时间。
contract BankWithSpeedBump { struct Withdrawal { uint amount; uint requestedAt; } mapping (address => uint) public balances; mapping (address => Withdrawal) public withdrawals; uint constant WAIT_PERIOD = 5 days; function deposit() public payable { balances[msg.sender] += msg.value; } function requestWithdrawal() public { if (balances[msg.sender] > 0) { uint amountToWithdraw = balances[msg.sender]; balances[msg.sender] = 0; withdrawals[msg.sender] = Withdrawal({ amount: amountToWithdraw, requestedAt: block.timestamp}); } } function withdraw() public { require(withdrawals[msg.sender].amount > 0, "No pending withdrawal"); require( block.timestamp > withdrawals[msg.sender].requestedAt + WAIT_PERIOD, "Wait period not completed" ); uint amount = withdrawals[msg.sender].amount; withdrawals[msg.sender].amount = 0; (bool sent, ) = msg.sender.call{value: amount}(""); require(sent, "Withdraw failed"); }}
3. 使用最新版本的 Solidity 编译器
始终使用最新版本的 Solidity 编译器。 新版本通常包含重要的安全修复和语言改进。 例如,Solidity 0.8.x 及以上版本引入了针对算术溢出和算术底溢出的内置保护措施,而算术溢出和算术底溢出以前是智能合约中的常见漏洞。
保持更新可确保您的代码受益于最新的安全功能和编译器检查。