Solidity 安全注意事项

本文总结了智能合约开发中常见的安全漏洞和最佳实践,包括重入攻击、算术精度问题、访问控制不当、非标准协议、原生代币处理、底层调用、随机数问题、存储槽管理以及编译器版本固定。同时,强调使用静态分析工具和编写全面测试的重要性。

1. 重入攻击

当合约在更新其状态之前进行外部调用时,会发生重入攻击。攻击者可以递归地调用关键函数来执行多次提款。这方面的一个经典例子是 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();
        }
    }
}

解决方案

  • 检查-生效-交互 模式:在转移资金之前更新状态。
  • 使用 OpenZeppelin 的 ReentrancyGuard modifier
function safeWithdraw() public nonReentrant {
    uint bal = balances[msg.sender];
    balances[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: bal}("");
    require(success);
}

2. 算术精度问题

由于 Solidity 没有浮点数,请确保你的数学结果不会向下舍入。如果需要更高的精度,请使用乘数。

//结果是错误的
uint x = 5 / 2; // 结果是 2,所有整数除法都向下舍入到最接近的整数

//使用乘数
uint multiplier = 10;
uint x = (5 * multiplier) / 2;

注意:除法应该始终是计算中的最后一个操作,以避免精度损失。

3. 访问控制

不正确的访问控制可能允许未经授权的用户执行敏感操作。这可能会导致巨大损失,尤其是在未经充分测试的简单操作中。

不安全的合约

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;
    }
}

4. 非标准协议

一些 token 实现并不完全符合以太坊标准。例如,USDT 并不严格遵循 ERC-20 标准,有些 NFT 不符合 ERC-721 或 ERC-1155。

对于 USDT,其 transfer 方法不返回值。在这种情况下,最好使用 safeTransferFrom

5. 原生 Token

当你的合约需要处理原生 token(例如 ETH)时,请确保避免在合约中留下过多的原生 token。你可以限制接口中转移的 value,并确保它符合预期。

6. 底层调用 (Low Level Call)

返回值检查calldelegateCall 都不会自动检查返回值。如果目标合约返回 false,合约状态可能会意外更改。因此,请始终手动检查返回值以确保调用成功。

(bool success, ) = target.call(data);
require(success, "Call failed");

7. 随机性

链上没有真正的随机性。如果安全性至关重要,请避免使用 timestamp 作为随机性来源,因为矿工可能会操纵它。相反,请使用像 Chainlink 这样的外部随机性来源。

8. 存储槽

合约存储在槽中组织,每个槽的大小为 32 字节,从 0 开始递增。如果你的合约是可升级的,则应将新变量添加到存储布局的末尾,而不是更改槽的顺序。这可能会导致重大问题。

重要提示:如果你的合约有继承关系,请避免更改父合约中的槽顺序。

9. 固定编译器版本

如果你的代码不打算在外部使用(作为库),请始终固定 Solidity 版本,例如 pragma solidity 0.8.26,而不是使用像 ^pragma solidity 0.8.26 这样的版本范围。这确保了其他审查你代码的人能够理解你使用的编译器版本,并避免不同编译器之间可能出现的问题。

其他重要提示

  • 使用像 Slither 这样的静态分析工具来扫描你的合约是否存在漏洞。
  • 为你的合约编写全面的测试,无论使用 Hardhat 还是 Foundry。确保你检查代码覆盖率,并包含高级测试以覆盖边缘情况,例如:

Foundry 高级测试 Part I — Fuzz 测试 1. 介绍 medium.com

Foundry 高级测试 Part II — Invariant 测试 在上个章节,我们讨论了 foundry fuzz 测试… medium.com

  • 原文链接: blog.blockmagnates.com/s...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
江湖只有他的大名,没有他的介绍。