Solidity 新手开发者需要注意的 5 个陷阱(以及如何避免它们)

本文总结了Solidity智能合约开发中常见的五个陷阱,包括存储、内存和calldata的区别,重入攻击,默认public的可见性,使用tx.origin进行授权的风险,以及无限循环/高Gas成本问题。针对每个问题,文章都给出了具体的代码示例和修复方案,旨在帮助开发者构建更安全、更智能的智能合约。

加粗“编写智能合约就像编写后端代码……只不过是在一个不可变的公共服务器上,每次有人与之交互都要付出真金白银。”加粗

欢迎来到Solidity。

Solidity功能强大,但也充满了陷阱,可能会烧掉开发者(有时,甚至会烧掉大量用户资金)。作为一个花时间构建智能合约的人,我见过几乎每个新的Solidity开发者都会遇到的常见陷阱。

以下是我希望在开始时就知道的 前5个陷阱

1️⃣ Storage vs Memory vs Calldata — 为什么Solidity使用三个数据位置:

storage — 链上持久数据(写入成本高)。

memory — 临时的,函数执行后会被擦除。

calldata — 只读,外部输入。

初学者经常误解 storage和memory 在Solidity中的工作方式。将一个storage变量分配给另一个storage变量可能导致两个变量指向相同的数据!

经验法则:

  • memory = 临时的,函数内副本。
  • storage = 持久的,全局状态。
  • calldata 是一个不可修改、非持久的区域,当一个函数从外部调用时,函数参数存储在该区域。与使用 memory 相比,它更便宜更节省gas,并且是只读的。 始终要注意你正在使用哪一个。

陷阱:

当你打算使用临时副本时,意外修改 storage 可能会导致错误或意外行为。

示例:

// 示例:storage vs memory
struct User {
    uint age;
}

User[] public users;

function updateUser(uint index) public {
    // 指向storage:修改实际的状态变量
    User storage u = users[index];
    u.age = 30; // ✅ 这会直接更新 users[index]

    // 创建一个 memory 副本:不会影响原始数据
    User memory temp = u;
    temp.age = 40; // ❌ 这不会更新 users[index]
}

修复:

始终要明确。如果你只需要读取外部输入,为了gas优化,最好使用 calldata

// ❌ 常见错误:为外部输入使用 memory
function processData(uint[] memory data) external {
    // 逻辑
}

// ✅ 更好的方法:使用 calldata 来节省 gas 和提高安全性
function processData(uint[] calldata data) external {
    // 逻辑
}

2️⃣ 重入攻击 — 经典陷阱

重入是指合约在更新其状态之前进行外部调用,而被调用的合约回调并扰乱你的逻辑。

示例:

function withdraw() public {
    uint amount = balances[msg.sender];
    (bool sent, ) = msg.sender.call{value: amount}(""); // 外部调用!
    require(sent, "Failed");
    balances[msg.sender] = 0; // ❌ 状态更新发生在之后
}

修复:

在外部调用 之前 更新状态,或使用 Checks-Effects-Interactions 模式。

function withdraw() public {
    uint amount = balances[msg.sender];
    balances[msg.sender] = 0; // ✅ 先更新
    (bool sent, ) = msg.sender.call{value: amount}("");
    require(sent, "Failed");
}

或者更好的是,使用 OpenZeppelin 的 ReentrancyGuard

3️⃣ 可见性:默认Public(哎呀!)

Solidity的函数默认是 public 的,这意味着任何人(包括机器人)都可以调用它们,除非你另行指定。

陷阱: 忘记声明一个函数为 privateinternal 意味着它可能被滥用。

示例:

function mint() {
    _mint(msg.sender, 1000); // 😬 任何人都可以铸造!
}

修复:

始终显式声明可见性:privateinternalexternalpublic

4️⃣ 使用 tx.origin 进行授权(不要)

许多新的开发者使用 tx.origin 来检查谁在调用该函数。但这很危险。

为什么?

tx.origin 指的是交易的原始调用者,即使它是通过另一个合约发起的。它会引发网络钓鱼式攻击。

示例:

function onlyOwner() public {
    require(tx.origin == owner, "Not owner"); // ❌ 危险
}

如果一个恶意合约诱骗所有者调用它,它可以转发该调用并绕过你的检查。

修复:

使用 msg.sender 进行访问控制。始终如此。

require(msg.sender == owner, "Not owner"); // ✅

5️⃣ 无限循环 / 高Gas成本

Solidity 在处理无界循环时表现不佳。由于以太坊具有区块gas限制,因此运行时间过长的循环可能会使你的合约 无法使用

示例:

function sendToAll() public {
    for (uint i = 0; i < users.length; i++) {
        users[i].transfer(1 ether); // 😵 太多迭代 = 失败
    }
}

修复:

避免随着存储扩展的循环。考虑:

使用批处理

让用户“拉取”资金而不是推送资金

尽可能转移到链下

✍️ 最后的想法

Solidity 就像区块链的 C 语言。低级别的控制意味着更多的权力,但也意味着更多的责任。这五个陷阱仅仅是个开始,但它们将帮助你构建更安全、更智能的智能合约。

希望我写关于这些主题的深入探讨吗?请在评论中告诉我。

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

0 条评论

请先 登录 后评论
CoinsBench
CoinsBench
https://coinsbench.com/