本文总结了智能合约开发中常见的安全漏洞和最佳实践,包括重入攻击、算术精度问题、访问控制不当、非标准协议、原生代币处理、底层调用、随机数问题、存储槽管理以及编译器版本固定。同时,强调使用静态分析工具和编写全面测试的重要性。
当合约在更新其状态之前进行外部调用时,会发生重入攻击。攻击者可以递归地调用关键函数来执行多次提款。这方面的一个经典例子是 The DAO 攻击,该攻击导致了 6000 万美元的损失。
易受攻击的合约
contract VulnerableBank {
mapping(address => uint) balances;
function withdraw() public {
uint bal = balances[msg.sender];
(bool success, ) = msg.sender.call{value: bal}("");
require(success, "Transfer failed");
balances[msg.sender] = 0;
}
}
contract Attacker {
function attack() external payable {
bank.withdraw();
}
fallback() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw();
}
}
}
解决方案:
function safeWithdraw() public nonReentrant {
uint bal = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: bal}("");
require(success);
}
由于 Solidity 没有浮点数,请确保你的数学结果不会向下舍入。如果需要更高的精度,请使用乘数。
//结果是错误的
uint x = 5 / 2; // 结果是 2,所有整数除法都向下舍入到最接近的整数
//使用乘数
uint multiplier = 10;
uint x = (5 * multiplier) / 2;
注意:除法应该始终是计算中的最后一个操作,以避免精度损失。
不正确的访问控制可能允许未经授权的用户执行敏感操作。这可能会导致巨大损失,尤其是在未经充分测试的简单操作中。
不安全的合约
contract UnsafeAdmin {
address public owner;
function changeOwner(address newOwner) public {
owner = newOwner;
}
}
contract SecureAdmin {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
function changeOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
}
一些 token 实现并不完全符合以太坊标准。例如,USDT 并不严格遵循 ERC-20 标准,有些 NFT 不符合 ERC-721 或 ERC-1155。
对于 USDT,其 transfer 方法不返回值。在这种情况下,最好使用 safeTransferFrom
。
当你的合约需要处理原生 token(例如 ETH)时,请确保避免在合约中留下过多的原生 token。你可以限制接口中转移的 value
,并确保它符合预期。
返回值检查:call
和 delegateCall
都不会自动检查返回值。如果目标合约返回 false
,合约状态可能会意外更改。因此,请始终手动检查返回值以确保调用成功。
(bool success, ) = target.call(data);
require(success, "Call failed");
链上没有真正的随机性。如果安全性至关重要,请避免使用 timestamp
作为随机性来源,因为矿工可能会操纵它。相反,请使用像 Chainlink 这样的外部随机性来源。
合约存储在槽中组织,每个槽的大小为 32 字节,从 0 开始递增。如果你的合约是可升级的,则应将新变量添加到存储布局的末尾,而不是更改槽的顺序。这可能会导致重大问题。
重要提示:如果你的合约有继承关系,请避免更改父合约中的槽顺序。
如果你的代码不打算在外部使用(作为库),请始终固定 Solidity 版本,例如 pragma solidity 0.8.26
,而不是使用像 ^pragma solidity 0.8.26
这样的版本范围。这确保了其他审查你代码的人能够理解你使用的编译器版本,并避免不同编译器之间可能出现的问题。
Foundry 高级测试 Part I — Fuzz 测试 1. 介绍 medium.com
Foundry 高级测试 Part II — Invariant 测试 在上个章节,我们讨论了 foundry fuzz 测试… medium.com
- 原文链接: blog.blockmagnates.com/s...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!