WTF-Solidity 安全指南:Consensys 给 Solidity 程序员的 16 条智能合约安全建议(全量翻译与源码级注解)
2026/9/16 0:03:11 网站建设 项目流程

WTF-Solidity 安全指南:Consensys 给 Solidity 程序员的 16 条智能合约安全建议(全量翻译与源码级注解)

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

导读

本文完整收录并详解 MetaMask 项目方 Consensys 于 2020 年发布的《Solidity Best Practices for Smart Contract Security》博客译文(由 WTF-Solidity 作者 0xAA 翻译整理),系统梳理了面向 Solidity 程序员的 16 条安全开发建议,包括assert/require/revert的正确用法、modifier使用禁忌、回退函数陷阱、tx.origin授权风险、时间戳依赖、多重继承线性化、extcodesize绕过等。每一条建议都结合当前 WTF-Solidity 仓库中的源码实例(如 15_Errors、19_Fallback、S12_TxOrigin 等)进行二次注解,并标明 Solidity 0.5 到 0.8 之间的版本差异。读完本文,你将掌握一套可直接用于审计与编写生产级合约的 Check-Effects-Interactions 安全范式。

原文撰写于 2020 年 8 月(Solidity 版本尚停留在 0.5),如今 Solidity 已迭代到 0.8.x,部分函数写法发生了变化,但绝大多数安全理念至今仍然适用。译文已逐条标明版本差异可能导致的坑,供中文开发者学习。

一、背景:为什么需要一份"语言级"安全清单

如果你已经牢记智能合约安全理念(如 CEI 模式、最小权限、审计日志),并且正在处理 EVM 的特性,那么是时候考虑一些特定于 Solidity 编程语言的安全模式了。Consensys 的这份综述聚焦于 Solidity 的安全开发建议,这些建议也对用其他语言开发智能合约具有指导意义。

WTF-Solidity 仓库正是以"代码 + 极简讲解"的方式系统实践这些理念:教程主体位于 01_HelloWeb3 至 57_Flashloan,安全专题(重入、选择器冲突、中心化风险、时间操纵、预言机操纵等)位于 S01_ReentrancyAttack 至 S17_CrossReentrancy。本文的每一条建议都能在仓库中找到对应的实战样例。

二、第 1 条:正确使用assert()require()revert()

便利函数assertrequire可用于检查条件,如果条件不满足则抛出异常。二者的使用场景必须严格区分:

  • assert只能用于测试内部错误和检查不变量(invariant)。当assert失败时,往往意味着代码中存在真实的 bug(Solidity 0.8 之前会消耗全部剩余 gas)。
  • require用于确保满足有效条件,例如校验输入或合约状态变量,或者验证来自外部合约调用的返回值。
  • 0xAA 注:Solidity 在 0.8.4 版本引入自定义error功能。0.8.4 之前用require检查有效条件,之后推荐用revert-error(自定义 error)来确保满足有效条件,以节省 gas。

遵循这种范式可以让形式化分析工具验证"无效操作码永远不会被执行":这意味着代码中没有不变量被违反,且可以被形式化验证。

pragma solidity ^0.5.0; contract Sharer { function sendHalf(address payable addr) public payable returns (uint balance) { require(msg.value % 2 == 0, "偶数required."); // Require() 可以加一个自定义消息 uint balanceBeforeTransfer = address(this).balance; (bool success, ) = addr.call.value(msg.value / 2)(""); require(success); // 如果success为false,就revert。下面的总是成立。 assert(address(this).balance == balanceBeforeTransfer - msg.value / 2); // used for internal error checking return address(this).balance; } }

仓库源码佐证:三种写法的 gas 成本对比

WTF-Solidity 的 15_Errors/Error.sol 用同一业务场景(校验 Token 所有者)对比了三种写法在 Remix 0.8.17 编译下的 gas 消耗:

方式代码Gas 成本
自定义 errorrevert TransferNotOwner();24457
require + 字符串require(_owners[tokenId] == msg.sender, "Transfer Not Owner");24755
assertassert(_owners[tokenId] == msg.sender);24473

可以看出,自定义error是最省 gas 的失败处理方式(字符串类型的 revert 原因最贵),同时它还能携带参数(如revert TransferNotOwner(msg.sender))方便链下排查。这也是 0.8.4 之后社区普遍推荐error/revert取代require的原因之一。

三、第 2 条:modifier仅用于检查

修饰符(modifier)内的代码通常在函数体之前执行,因此任何状态更改或外部调用都会违反Checks-Effects-Interactions(检查-生效-交互)模式。此外,开发人员可能不会注意到修饰符中的语句,因为修饰符的代码可能远离函数声明。例如,修饰符中的外部调用可能导致重入攻击:

contract Registry { address owner; function isVoter(address _addr) external returns(bool) { // Code } } contract Election { Registry registry; modifier isEligible(address _addr) { require(registry.isVoter(_addr)); _; } function vote() isEligible(msg.sender) public { // Code } }

在这种情况下,Registry合约可以通过在isVoter()内部调用Election.vote()进行重入攻击。

注意:使用modifier替换多个函数中的重复条件检查(例如isOwner())是合理的;否则请在函数内部使用requirerevert。这会使智能合约代码更具可读性、更易于审计。

仓库源码佐证:标准的 onlyOwner 写法

WTF-Solidity 的 11_Modifier/Owner.sol 展示了"仅做检查"的规范写法——修饰符内只有一条require状态检查,随后立即用_;占位符进入函数体:

modifier onlyOwner { require(msg.sender == owner); // 检查调用者是否为owner地址 _; // 如果是的话,继续运行函数主体;否则报错并revert交易 } function changeOwner(address _newOwner) external onlyOwner{ owner = _newOwner; }

这就是把"权限检查"这一横切关注点抽离成modifier的最佳实践:修饰符内不做任何外部调用与状态修改,只做校验

四、第 3 条:注意整数除法的舍入

所有整数除法都向下舍入到最接近的整数(向零取整)。如果需要更高的精度,请考虑使用乘数(multiplier),或同时存储分子和分母。将来 Solidity 会有浮点类型,这会让处理更简单。

// bad uint x = 5 / 2; // Result is 2, all integer division rounds DOWN to the nearest integer

使用乘数可以防止四舍五入带来的精度损失,在将来使用x时需要考虑这个乘数:

// good uint multiplier = 10; uint x = (5 * multiplier) / 2;

存储分子和分母意味着你可以计算 numerator/denominator 链下的精确结果:

// good uint numerator = 5; uint denominator = 2;

五、第 4 条:注意抽象合约abstract和接口interface之间的权衡

接口和抽象合约都为智能合约提供了一种可定制、可重用的设计方式。Solidity 0.4.11 引入的接口类似于抽象合约,但不能实现任何功能。接口还有一些限制,例如不能访问存储、不能从其他接口继承(注:现代 Solidity 已允许接口继承接口),这通常使抽象合约更实用。尽管如此,接口对于在实现之前设计合约契约仍然很有用。

此外,重要的是要记住:如果合约继承自抽象合约,它必须通过覆盖(override)实现所有未实现的功能,否则它自身也必须是抽象的。

仓库源码佐证:接口与抽象合约的实战

WTF-Solidity 的 14_Interface/Interface.sol 同时给出了两种形态:

abstract contract InsertionSort{ function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory); } interface IERC721 is IERC165 { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); function balanceOf(address owner) external view returns (uint256 balance); function safeTransferFrom(address from, address to, uint256 tokenId) external; // ... }
  • 接口IERC721只声明事件与函数签名,不含任何实现,并通过IERC721(0xBC4C...)这种地址强转方式与链上 BAYC 合约交互(见 Interface.sol)。
  • 抽象合约InsertionSort则只声明函数,把排序实现留给子类去完成。

二者的选择原则:需要约束"是什么"用接口,需要复用"怎么做"用抽象合约

六、第 5 条:Fallback 后备函数

0xAA 注:Solidity 0.5.0 时还没有receive()函数,且fallback当时直接声明为function()。关于最新版本fallback/receive的详细教程,请见本仓库 19_Fallback/readme.md。

5.1 保持 fallback function 简单

当合约被发送一个没有参数的消息(或者没有函数匹配调用时),fallback function会被调用。当被.send().transfer()触发时,fallback function只能访问2300 gas。如果你希望能够从send()transfer()接收 ETH,那么在后备函数中最多可以做的就是记录一个事件。如果需要计算更多 gas,请使用适当的函数(或改用.call):

// bad function() payable { balances[msg.sender] += msg.value; } // good function deposit() payable external { balances[msg.sender] += msg.value; } function() payable { require(msg.data.length == 0); emit LogDepositReceived(msg.sender); }

5.2 检查回退函数中的数据长度

由于fallback function不仅在普通以太传输(没有msg.data)时被调用,也会在没有其他函数匹配时被调用。如果后备函数仅用于记录接收到的 ETH,则应检查数据是否为空。否则,如果你的合约被错误调用(调用了不存在的函数),调用者将不会注意到任何异常:

// bad function() payable { emit LogDepositReceived(msg.sender); } // good function() payable { require(msg.data.length == 0); emit LogDepositReceived(msg.sender); }

仓库源码佐证:0.8 时代的 receive/fallback 分工

现代 Solidity(0.6.0+)将两者拆分为receive()fallback(),调用逻辑见 19_Fallback/Fallback.sol 中的注释流程图:

接收ETH → msg.data是空? → 是 → receive()存在? → 存在则 receive(),否则 fallback() → 否 → fallback()
// 接收ETH时释放Received事件 receive() external payable { emit receivedCalled(msg.sender, msg.value); } // fallback fallback() external payable{ emit fallbackCalled(msg.sender, msg.value, msg.data); }

与之配套,20_SendETH/SendETH.sol 明确标注了三种转账方式的 gas 差异,这直接决定了回退函数能做什么:

方式Gas 上限失败行为
transfer2300 gasrevert
send2300 gas返回 bool
call全部 gas返回 (bool, data)

这就是"fallback 只能记录事件"的根本原因——2300 gas 不足以执行状态写入之外更复杂的逻辑。

七、第 6 条:显式标记应付函数和状态变量

从 Solidity 0.4.0 开始,每个接收以太币的函数都必须使用payable修饰符,否则如果交易带有msg.value > 0将被 revert。

注意一个不明显的事实payable修饰符仅适用于来自external合约的调用。如果在同一个合约的payable函数中调用了一个非payable函数,这个非payable函数不会失败,尽管msg.value不为零。

八、第 7 条:显式标记函数和状态变量的可见性

明确标记函数和状态变量的可见性。函数可以指定为externalpublicinternalprivate。请理解它们之间的差异,例如external可能足以代替public。而对于状态变量,external是不适用的。明确标记可见性将更容易捕捉关于"谁可以调用函数或访问变量"的错误。

  1. external:函数是合约接口的一部分。external函数f不能在内部调用(即f()不工作,但this.f()工作)。外部函数在接收大量数据时效率更高。
  2. public:函数是合约接口的一部分,既可以在内部调用,也可以通过消息调用。对于公共状态变量,会生成一个自动 getter 函数。
  3. internal:函数和状态变量只能在内部访问,不使用this
  4. private:函数和状态变量仅对定义它们的合约可见,在派生合约中不可见。注意:合约内的所有内容对区块链外部的所有观察者都是可见的,即使是private变量(只能防止外部/子合约直接访问,不能保密数据)。
// bad uint x; // the default is internal for state variables, but it should be made explicit function buy() { // the default is public // public code } // good uint private y; function buy() external { // only callable externally or using this.buy() } function utility() public { // callable externally, as well as internally: changing this code requires thinking about both cases. } function internalAction() internal { // internal code }

九、第 8 条:将编译指示锁定到特定的编译器版本

合约应该使用与它们经过最多测试的相同编译器版本和编译标志来部署。锁定 pragma 有助于确保合约不会被意外部署——例如使用可能带有未被发现错误的最新编译器。合约也可能由其他人部署,而pragma会指示原作者预期的编译器版本:

// bad pragma solidity ^0.4.4; // good pragma solidity 0.4.4;

注意:浮动 pragma 版本(即^0.4.25)可能可以用0.4.26-nightly.2018.9.25编译,但不应使用 nightly 版本来编译生产代码。

警告:当合约打算供其他开发人员作为库使用(例如库或 EthPM 包中的合约)时,可以允许 pragma 语句浮动。否则,开发人员需要手动更新编译指示才能本地编译。

仓库源码佐证:WTF 教程的 pragma 实践

WTF-Solidity 各教程合约统一使用精确版本锁定写法,如 15_Errors/Error.sol 的pragma solidity ^0.8.34;。注意教程为了便于多版本学习仍保留了范围锁定,而生产合约则应采用"锁死主版本 + 最小补丁版本"策略,并配合foundry.toml(见 foundry.toml)中的编译器配置确保 CI 与本地构建一致。

十、第 9 条:使用事件来监控合约活动

部署后监控合约活动是很有必要的。一种实现方式是查看合约的所有交易,但这可能不够:合约之间的消息调用不会记录在区块链的交易列表中,而且交易只显示输入参数,不显示对状态进行的实际更改。事件也可用于触发用户界面中的功能。

contract Charity { mapping(address => uint) balances; function donate() payable public { balances[msg.sender] += msg.value; } } contract Game { function buyCoins() payable public { // 5% goes to charity charity.donate.value(msg.value / 20)(); } }

在这里,Game合约内部调用了Charity.donate(),该交易不会出现在Charity的外部交易列表中,而只在内部交易中可见。

事件是记录合约中发生的事情的便捷方式。发出的事件与其他合约数据一起留在区块链中,可供将来审计。这是对上述示例的改进,使用事件来提供慈善机构的捐赠历史:

contract Charity { // define event event LogDonate(uint _amount); mapping(address => uint) balances; function donate() payable public { balances[msg.sender] += msg.value; // emit event emit LogDonate(msg.value); } }

注意:优先使用更新的 Solidity 结构。首选别名,例如selfdestruct(而不是suicide)和keccak256(而不是sha3)。类似的模式require(msg.sender.send(1 ether))也可以简化为使用transfer(),如msg.sender.transfer(1 ether)

仓库源码佐证:ERC20 Transfer 事件

12_Event/Event.sol 展示了事件的标准用法——在状态变更后立即emit,且对from/to地址使用indexed便于链下按主题检索:

event Transfer(address indexed from, address indexed to, uint256 value); function _transfer(address from, address to, uint256 amount) external { _balances[from] -= amount; _balances[to] += amount; emit Transfer(from, to, amount); // 释放事件 }

这一模式正是 ERC20/ERC721 标准的通用实践,仓库中的 31_ERC20/ERC20.sol 与 34_ERC721/ERC721.sol 均遵循此规范。

十一、第 10 条:请注意,"内置"函数可能会被隐藏

目前可以在 Solidity 中隐藏内置的全局变量。这允许合约覆盖内置插件的功能,例如msgrevert()。尽管这是有意为之,但它可能会误导合约用户对合约的真实行为的认知:

contract PretendingToRevert { function revert() internal constant {} } contract ExampleContract is PretendingToRevert { function somethingBad() public { revert(); } }

合约用户(和审计员)应该了解他们打算使用的任何应用程序的完整智能合约源代码。

十二、第 11 条:避免使用 tx.origin

永远不要用tx.origin做授权。另一个合约可以有一个方法来调用你的合约(例如,用户存了一些资金),并且你的合约会授权该交易,因为发起交易的地址位于tx.origin

contract MyContract { address owner; function MyContract() public { owner = msg.sender; } function sendTo(address receiver, uint amount) public { require(tx.origin == owner); (bool success, ) = receiver.call.value(amount)(""); require(success); } } contract AttackingContract { MyContract myContract; address attacker; function AttackingContract(address myContractAddress) public { myContract = MyContract(myContractAddress); attacker = msg.sender; } function() public { myContract.sendTo(attacker, msg.sender.balance); } }

你应当使用msg.sender做授权(如果另一个合约调用你的合约,msg.sender将是该合约的地址,而不是调用该合约的用户的地址)。

警告:除了授权问题外,tx.origin将来有可能从以太坊协议中删除,因此使用tx.origin的代码将与未来版本不兼容(Vitalik:"不要假设tx.origin会继续存在")。此外,使用tx.origin还会限制合约之间的互操作性,因为合约永远不可能是tx.origin

仓库源码佐证:完整的钓鱼攻击复现

WTF-Solidity 的安全专题 S12_TxOrigin/PhishingWithTxOrigin.sol 完整复现了上述攻击:Banktx.origin == owner做鉴权,攻击合约Attack在构造期间强制把Bank地址转换为合约类型,一旦诱导 owner 调用attack()tx.origin仍等于 owner,银行余额便被全部转走:

function transfer(address payable _to, uint _amount) public { require(tx.origin == owner, "Not owner"); // 检查消息来源 —— 错误的鉴权方式 (bool sent, ) = _to.call{value: _amount}(""); require(sent, "Failed to send Ether"); }

对照教程 21_CallContract 可以更系统地理解合约间调用时msg.sendertx.origin的语义差异。

十三、第 12 条:时间戳依赖

使用时间戳执行合约中的关键功能时,有三个主要考虑因素,尤其是当操作涉及资金转移时。

时间戳操纵

请注意,区块的时间戳可以由矿工操纵。考虑这个合约:

uint256 constant private salt = block.timestamp; function random(uint Max) constant private returns (uint256 result){ //get the best seed for randomness uint256 x = salt * 100/Max; uint256 y = salt * block.number/(salt % 5) ; uint256 seed = block.number/3 + (salt % 300) + Last_Payout + y; uint256 h = uint256(block.blockhash(seed)); return uint256((h / x)) % Max + 1; //random number between 1 and Max }

当合约使用时间戳播种一个随机数时,矿工实际上可以在区块被验证后的 15 秒内发布一个时间戳,从而有效地允许矿工预先计算一个更有利于自己中奖概率的选项。时间戳不是随机的,不应在该上下文中使用。

仓库源码佐证:用时间戳做随机数 = 漏洞

安全专题 S07_BadRandomness 正是围绕"区块时间/区块号可被矿工操纵"这一弱点设计的。类似的,S14_TimeManipulation/src/TimeManipulation.sol 用block.timestamp % 170 == 0决定能否 mint NFT,说明依赖时间戳会引入可预测性与操控风险:

function luckyMint() external returns(bool success){ if(block.timestamp % 170 == 0){ _mint(msg.sender, totalSupply); totalSupply++; success = true; } else { success = false; } }

十四、第 13 条:15 秒规则

以太坊黄皮书(参考规范)没有规定区块可以在时间上漂移多少,但它确实规定每个时间戳应大于其父时间戳。流行的以太坊协议实现 Geth 和 Parity 都会拒绝未来时间戳超过 15 秒的区块。因此,评估时间戳使用的一个好的经验法则是:如果你与时间相关的事件规模可以变化 15 秒并保持完整性,那么可以使用block.timestamp

避免将 block.number 用作时间戳

可以使用block.number属性和平均出块时间来估计时间增量,但这并不是面向未来的可靠方案,因为出块时间可能会改变(例如分叉重组和难度炸弹)。不过在只持续几天的销售场景中,15 秒规则允许人们获得更可靠的时间估计。

十五、第 14 条:多重继承注意事项

在 Solidity 中使用多重继承时,理解编译器如何构成继承图非常重要:

contract Final { uint public a; function Final(uint f) public { a = f; } } contract B is Final { int public fee; function B(uint f) Final(f) public { } function setFee() public { fee = 3; } } contract C is Final { int public fee; function C(uint f) Final(f) public { } function setFee() public { fee = 5; } } contract A is B, C { function A() public B(3) C(5) { setFee(); } }

部署合约时,编译器将从右到左线性化继承(在关键字is之后,父项从最基类到最派生列出)。这是合约A的线性化:

Final <- B <- C <- A

线性化的结果是fee = 5,因为C是最接近派生合约的。这似乎很明显,但想象一下C能够隐藏关键函数、重新排序布尔子句并导致开发人员编写可利用合约的场景。静态分析目前不会对"被遮蔽的函数"发出警告,因此必须手动检查。

仓库源码佐证:钻石继承的线性化顺序

13_Inheritance/Inheritance.sol 与 13_Inheritance/DiamondInheritance.sol 是理解该机制的完整教材。钻石继承示例中people is Adam, Eve,线性化顺序为God <- Adam <- Eve <- people,因此people.foo()中调用super.foo()实际执行的是Eve.foo,而Eve.foo里再super.foo()才会回到God.foo

God / \ Adam Eve \ / people

要覆盖同名函数时,必须在override中显式列出所有父合约,例如function foo() public override(Adam, Eve)——这与本建议中"多重继承必须手动检查线性化顺序"的告诫完全对应。仓库中还有 13_Inheritance/ModifierInheritance.sol 演示修饰符继承的叠加顺序。

十六、第 15 条:使用接口类型而不是地址来保证类型安全

当函数将合约地址作为参数时,最好传递接口或合约类型而不是纯address。因为如果函数在源代码的其他地方被调用,编译器会提供额外的类型安全保证:

contract Validator { function validate(uint) external returns(bool); } contract TypeSafeAuction { // good function validateBet(Validator _validator, uint _value) internal returns(bool) { bool valid = _validator.validate(_value); return valid; } } contract TypeUnsafeAuction { // bad function validateBet(address _addr, uint _value) internal returns(bool) { Validator validator = Validator(_addr); bool valid = validator.validate(_value); return valid; } }

从下面的示例可以看出使用TypeSafeAuction合约的好处。如果validateBet()使用address参数而非Validator合约类型,编译器将抛出类型错误:

contract NonValidator{} contract Auction is TypeSafeAuction { NonValidator nonValidator; function bet(uint _value) { bool valid = validateBet(nonValidator, _value); // TypeError: Invalid type for argument in function call. // Invalid implicit conversion from contract NonValidator // to contract Validator requested. } }

十七、第 16 条:避免用extcodesize检查外部拥有账户

以下修饰符(或类似的检查)通常用于验证调用是来自外部拥有账户(EOA)还是合约账户:

// bad modifier isNotContract(address _a) { uint size; assembly { size := extcodesize(_a) } require(size == 0); _; }

这个想法很简单:如果一个地址包含代码,它就不是 EOA,而是合约账户。但是,合约在构建(构造函数执行)期间没有可用的源代码。这意味着在构造函数运行时,它可以调用其他合约,但extcodesize在它的地址上返回零。下面是一个最小的绕过示例:

contract OnlyForEOA { uint public flag; // bad modifier isNotContract(address _a){ uint len; assembly { len := extcodesize(_a) } require(len == 0); _; } function setFlag(uint i) public isNotContract(msg.sender){ flag = i; } } contract FakeEOA { constructor(address _a) public { OnlyForEOA c = OnlyForEOA(_a); c.setFlag(1); } }

因为可以预先计算合约地址,所以如果检查的是"在 block n 处为空、但在 block n 之后被部署"的合约,依然会失败。

警告:这个问题很微妙。如果目标是阻止其他合约调用你的合约,那么extcodesize检查可能足够了。另一种方法是检查tx.origin == msg.sender,尽管这也有缺点。在其他场景下extcodesize或许有用,但需要理解 EVM 的基本行为并自行判断。

仓库源码佐证:合约账户检测的攻防演练

安全专题 S08_ContractCheck 完整收录了基于extcodesize的 EOA 校验及其绕过手法(构造函数内调用即可骗过检查),可配合本文第 16 条一起研读。

十八、总结:从 16 条建议到系统化安全实践

Consensys 的这 16 条建议可以归纳为三个层次,与 WTF-Solidity 教程 + 安全专题的编排一一对应:

  1. 语言级纪律(第 1~10 条):正确使用assert/require/revertmodifier只做检查、fallback 保持简单、显式可见性与payable、锁定 pragma、事件监控、警惕内置函数被遮蔽——这些属于"不写错代码"的底线,对应教程 15_Errors、11_Modifier、19_Fallback、12_Event 等。
  2. 信任模型级风险(第 11~13 条):tx.origin授权、时间戳随机数、15 秒规则——这些属于"链上环境不可信"的认知,对应安全专题 S12_TxOrigin、S07_BadRandomness、S14_TimeManipulation。
  3. 架构设计级风险(第 14~16 条):多重继承线性化、类型安全、extcodesize绕过——这些属于"EVM 语义陷阱",对应教程 13_Inheritance、14_Interface 与安全专题 S08_ContractCheck。

值得再次强调版本适配:本文示例代码按 2020 年(Solidity 0.5)撰写,在 0.8.x 下需注意——assert/require失败不再退还全部 gas(0.8 起默认溢出检查由编译器内建)、fallback已拆分为receive/fallbackselfdestruct替代suicidekeccak256替代sha3、自定义error成为推荐的失败处理方式。安全理念本身不随版本变化,但落地写法请以仓库中标注 0.8.34 的现代示例为准。

对于希望进一步系统化学习安全攻防的读者,建议按仓库目录顺序研读安全专题 S01_ReentrancyAttack 至 S17_CrossReentrancy,并配合 scripts/run-forge-tests.sh 与 foundry.toml 搭建本地测试环境,把每条建议都变成可运行、可验证的测试用例。

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询