CryptoZombies 旅程:安全、所有权与 Gas 优化

本文深入探讨了智能合约开发中安全、所有权和 Gas 优化这三个关键要素。通过 CryptoZombies 的案例,强调了使用 OpenZeppelin 的 Ownable 合约进行访问控制的重要性,以及如何通过结构体打包和 view 函数等技术优化 Gas 成本。总结了专业 Solidity 开发者需要具备的安全和效率意识。

欢迎回来,各位区块链建造者们!如果你们和我一样,最初让智能合约运行起来的兴奋感之后,很快就会面临一个令人望而却步的问题:“这东西安全吗?我的用户使用它会花费一大笔钱吗?”

我们在 CryptoZombies 的旅程已经带领我们从基本语法到达了使以太坊开发独特而强大的核心。在这次深入探讨中,我们将涵盖专业智能合约开发中不可协商的三位一体:安全性、所有权和 gas 优化。让我们动手编写一些代码。

第 1 部分:不可变的堡垒与控制的需求

在传统的 Web 开发中,你可以在周二下午修复服务器上的一个错误。在以太坊上,部署的智能合约是不可变的。你无法更改它。这是去信任化的一个核心特性,但对于不可预见的错误来说,却是一场噩梦。

ZombieFeeding 合约存在一个关键缺陷:它硬编码了 CryptoKitties 合约地址。如果该地址发生了变化怎么办?我们的解决方案是使其可更新:

// 易受攻击的代码 - 请勿使用
function setKittyContractAddress(address _address) external {
    kittyContract = KittyInterface(_address);
}

问题:这个函数是 external,这意味着任何人都可以调用它。恶意攻击者可以将我们的合约指向一个虚假的、损坏的合约,从而永久性地破坏我们所有人的游戏。这是一个典型的缺少访问控制检查的例子。

第 2 部分:使用 Ownable 实现访问控制

解决方案不是重新发明轮子。而是使用来自 OpenZeppelin 的经过实战考验、社区审计的代码,OpenZeppelin 是安全智能合约的行业标准。 他们的 Ownable 合约提供了一个简单而有效的所有权模型。

  1. 继承:

我们首先在我们的基本合约中继承 Ownable

contract ZombieFactory is Ownable {
    // ... 我们的代码 ...
}

通过声明 is OwnableZombieFactory(以及继承自它的 ZombieFeeding)获得了 Ownable 合约的所有功能。

  1. 神奇的 Modifier:

Ownable 合约内部,核心逻辑是这个 modifier:

solidity

modifier onlyOwner() {
    require(isOwner()); // isOwner() 是一个检查 msg.sender == _owner 的函数
    _; // 这个符号的意思是“运行函数的其余部分”
}
  1. 保护我们的函数:

现在,我们用一个简单而强大的 modifier 锁定关键函数。

// 安全的代码
function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
}

它是如何运作的:当 setKittyContractAddress 被调用时,onlyOwner 中的代码首先运行。如果 require(isOwner()) 通过(即,调用者是合约的部署者),_; 告诉 Solidity 继续运行函数的其余部分。如果失败,交易会回滚。

最小权限原则:我们还通过使 feedAndMultiply 函数成为 internal 而不是 public 来应用此原则,因为它仅由合约中的另一个函数调用。 始终使用最严格的可见性。 这减少了你的“攻击面”,并且是安全开发的基石。

第 3 部分:以太坊的经济学——Gas 优化

为什么 Gas 很重要:每次存储操作都会花费用户真金白银,因为它会永久改变区块链,这是一个共享的全球数据库。 我们的目标是最大限度地降低这种成本。

优化技术 1:Struct Packing

看看我们最初的 Zombie 结构体。 它使用 uint 作为 level 和 cooldown,每个都消耗一个完整的 256 位存储槽。 由于 level 和时间戳不需要那么多空间,我们可以将它们打包。

// 之前:效率低下
struct Zombie {
    string name;
    uint dna;
    uint level;      // 256 位
    uint readyTime;  // 256 位
}
// 总计:4+ 存储槽
// 之后:Gas 优化
struct Zombie {
    string name;
    uint dna;
    uint32 level;     // 32 位
    uint32 readyTime; // 32 位
}
// 总计:3+ 存储槽! level 和 readyTime 被打包到 1 个槽中。

通过使用 uint32 并将它们放在一起,我们将这两个变量的存储成本降低了一半。 每次创建或更新僵尸时,此节省都会生效。

优化技术 2:view 函数和内存

getZombiesByOwner 函数是一个只读函数; 它不更改任何数据。 我们通过将其声明为 view 来向以太坊网络发出信号,这允许用户免费调用它(不收取 gas 费)。

此外,我们在 memory 中构建结果数组,而不是在 storage 中。

function getZombiesByOwner(address _owner) external view returns (uint[] memory) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    // ... 填充数组的逻辑 ...
    return result;
}

关键见解:在 view 函数内的临时 memory 中构建数组对用户来说是免费的。 另一种选择是在 storage (mapping (address => uint[])) 中维护一个并行数组——每次创建或转移僵尸时,都会花费我们数千 gas 的写入操作。 这是一个漂亮的例子,说明了如何用少量、免费的计算成本来换取永久存储成本的大量节省。

结论

考虑安全性和 gas 不是开发的次要阶段; 它是基础。 通过使用经过实战考验的模式(如 Ownable),坚持最小权限原则,并采用 gas 优化技术(如 struct packing 和 view 函数),你不再只是编写代码,而是开始设计健壮、高效且用户友好的 DApp。 这种心态是将业余爱好者与专业 Solidity 开发人员区分开来的原因。

但我们的旅程尚未结束。 我们已经保护了我们的堡垒并使其高效,但游戏需要的不仅仅是这些。 它需要灵魂、进展和引人入胜的机制。

在下一篇文章中,我们将把这些基础知识付诸实践。 我们将深入研究实现核心游戏机制,例如僵尸冷却、等级门控能力以及在链上高效检索用户整个僵尸军队的巧妙解决方案。 我们将看到我们今天学到的 gas 感知原则如何直接影响我们明天构建的游戏逻辑。

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

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block