本文总结了Solidity智能合约开发中常见的五个陷阱,包括存储、内存和calldata的区别,重入攻击,默认public的可见性,使用tx.origin进行授权的风险,以及无限循环/高Gas成本问题。针对每个问题,文章都给出了具体的代码示例和修复方案,旨在帮助开发者构建更安全、更智能的智能合约。
加粗“编写智能合约就像编写后端代码……只不过是在一个不可变的公共服务器上,每次有人与之交互都要付出真金白银。”加粗
欢迎来到Solidity。
Solidity功能强大,但也充满了陷阱,可能会烧掉开发者(有时,甚至会烧掉大量用户资金)。作为一个花时间构建智能合约的人,我见过几乎每个新的Solidity开发者都会遇到的常见陷阱。
以下是我希望在开始时就知道的 前5个陷阱。
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 {
// 逻辑
}
重入是指合约在更新其状态之前进行外部调用,而被调用的合约回调并扰乱你的逻辑。
示例:
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
。
Solidity的函数默认是 public 的,这意味着任何人(包括机器人)都可以调用它们,除非你另行指定。
陷阱: 忘记声明一个函数为 private
或 internal
意味着它可能被滥用。
示例:
function mint() {
_mint(msg.sender, 1000); // 😬 任何人都可以铸造!
}
修复:
始终显式声明可见性:private
、 internal
、 external
或 public
。
tx.origin
进行授权(不要)许多新的开发者使用 tx.origin
来检查谁在调用该函数。但这很危险。
为什么?
tx.origin
指的是交易的原始调用者,即使它是通过另一个合约发起的。它会引发网络钓鱼式攻击。
示例:
function onlyOwner() public {
require(tx.origin == owner, "Not owner"); // ❌ 危险
}
如果一个恶意合约诱骗所有者调用它,它可以转发该调用并绕过你的检查。
修复:
使用 msg.sender
进行访问控制。始终如此。
require(msg.sender == owner, "Not owner"); // ✅
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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!