{"content":{"title":"智能合约安全 - 常见漏洞（第二篇）","body":"* 原文链接： https://www.rareskills.io/post/smart-contract-security\r\n* 译文出自：[登链翻译计划](https://github.com/lbc-team/Pioneer)\r\n* 译者：[翻译小组](https://learnblockchain.cn/people/412)  校对：[Tiny 熊](https://learnblockchain.cn/people/15)\r\n* 本文永久链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/5860)\r\n\r\n![image-20230523155639451](https://img.learnblockchain.cn/pics/20230523155640.png)\r\n\r\n我们在这个系列中，将列出 Solidity 智能合约中一些容易反复出现的问题和漏洞。\r\n\r\n参考[上一篇](https://learnblockchain.cn/article/5853)，\r\n\r\n\r\n\r\n## 不安全的随机数\r\n\r\n\r\n\r\n目前不可能用区块链上的单一交易安全地产生随机数。区块链需要是完全确定的，否则分布式节点将无法达成关于状态的共识。因为它们是完全确定的，所以任何 \"随机\"的数字都可以被预测到。下面的掷骰子函数可以被利用。\r\n\r\n```solidity\r\ncontract UnsafeDice {\r\n    function randomness() internal returns (uint256) {\r\n        return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1);\r\n    }\r\n\r\n    // our dice can land on one of {0,1,2,3,4,5}function rollDice() public payable {\r\n        require(msg.value == 1 ether);\r\n\r\n        if (randomness() % 6) == 5) {\r\n            msg.sender.call{value: 2 ether}(\"\");\r\n        }\r\n    }\r\n}\r\n\r\ncontract ExploitDice {\r\n    function randomness() internal returns (uint256) {\r\n        return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1);\r\n    }\r\n\r\n    function betSafely(IUnsafeDice game) public payable {\r\n        if (randomness % 6) == 5)) {\r\n            game.betSafely{value: 1 ether}()\r\n        }\r\n\r\n        // else don't do anything\r\n    }\r\n}\r\n```\r\n\r\n如何来产生随机数并不重要，因为攻击者可以完全复制它。使用更多的 \"熵\"的来源，如msg.sender、时间戳等，不会有任何影响，因为智能合约也可以预测它。\r\n\r\n## 错误使用 Chainlink 随机数 Oracle \r\n\r\n\r\n\r\nChainlink是一个流行的解决方案，以获得安全的随机数。它分两步进行。首先，智能合约向预言机处发送一个随机数请求，然后在一些区块之后，预言机以一个随机数作为回应。\r\n\r\n由于攻击者无法预测未来，所以他们无法预测随机数。\r\n\r\n除非智能合约错误地使用预言机：\r\n\r\n- 请求随机数的智能合约必须在随机数返回之前不做任何事情。否则，攻击者可以监视返回随机数的预言机的mempool，并在前面运行预言机，知道随机数会是什么。\r\n- 随机数预言机本身可能会试图操纵你的应用程序。如果没有其他节点的共识，他们不能挑选随机数，但如果你的应用程序同时请求几个随机数，他们可以扣留和重新排序。\r\n- 最终性在[以太坊](https://www.rareskills.io/post/solidity-precompiles)或大多数其他EVM链上不是即时的。仅仅因为某些区块是最新的，并不意味着它不一定会保持这种状态。这被称为 \"链上重组\"。事实上，链可以改变的不仅仅是最后一个区块。这就是所谓的 \"深度重组\"。Etherscan报告了各种链的re-orgs，例如以太坊重组和Polygon 重组。在Polygon上，重组的深度可以达到30个或更多的区块，所以等待更少的区块会使应用变得脆弱（当zk-evm成为Polygon上的标准共识时，这种情况可能会改变，因为最终性将与以太坊的一致，但这是未来的预测，而不是目前的事实）。\r\n- 下面是其他 Chainlink 随机数的安全考虑。\r\n\r\n## 从价格Oracle中获取陈旧的数据\r\n\r\nChainlink 没有SLA（服务水平协议）来保持它的价格预言机在一定时间范围内的更新。当链上的交易严重拥堵时，价格更新可能会被延迟。\r\n\r\n使用价格预言机的智能合约必须明确地检查数据是否陈旧，即最近在某个阈值内被更新。否则，它不能对价格做出可靠的决策。\r\n\r\n还有一个更复杂的问题，如果价格没有变化超过[偏差阈值](https://blog.chain.link/chainlink-price-feeds-secure-defi/)，预言机可能不会更新价格以节省Gas，所以这可能会影响到什么时间阈值被认为是 \"陈旧\"。\r\n\r\n了解智能合约所依赖的 Oracle 的服务水平协议是很重要的。\r\n\r\n## 只依赖一个预言机\r\n\r\n无论一个预言机看起来多么安全，将来都可能发现攻击。对此的唯一防御措施是使用多个独立的预言机。\r\n\r\n## 一般来说，预言机是很难正确的\r\n\r\n区块链可以是相当安全的，但首先把数据放到链上就必须进行某种链外操作，这就放弃了区块链提供的所有安全保证。即使预言机者保持诚实，他们的数据来源也可以被操纵。例如，一个信使可以可靠地报告来自中心化交易所的价格，但这些价格可以被大量的买入和卖出订单所操纵。同样，依赖于传感器数据或一些web2 API的预言机也会受到传统黑客攻击的影响。\r\n\r\n一个好的智能合约架构在可能的情况下会完全避免使用预言机。\r\n\r\n## 混合计算\r\n\r\n考虑以下合约\r\n\r\n```solidity\r\ncontract MixedAccounting {\r\n    uint256 myBalance;\r\n\r\n    function deposit() public payable {\r\n        myBalance = myBalance + msg.value;\r\n    }\r\n\r\n    function myBalanceIntrospect() public view returns (uint256) {\r\n        return address(this).balance;\r\n    }\r\n\r\n    function myBalanceVariable() public view returns (uint256) {\r\n        return myBalance;\r\n    }\r\n\r\n    function notAlwaysTrue() public view returns (bool) {\r\n        return myBalanceIntrospect() == myBalanceVariable();\r\n    }\r\n}\r\n```\r\n\r\n上面的合约没有接收或回退函数，所以直接将以太传送给它就会回退。然而，合约可以用自毁的方式强行向它发送以太。在此案例中，`myBalanceIntrospect()`会比 `myBalanceVariable()` 大。两种以太币的计算方法都没有问题，但如果你同时使用这两种方法，那么合约可能会有不一致的行为。\r\n\r\n这同样适用于ERC20代币。\r\n\r\n```solidity\r\ncontract MixedAccountingERC20 {\r\n\r\n    IERC20 token;\r\n    uint256 myTokenBalance;\r\n\r\n    function deposit(uint256 amount) public {\r\n        token.transferFrom(msg.sender, address(this), amount);\r\n        myTokenBalance = myTokenBalance + amount;\r\n    }\r\n\r\n    function myBalanceIntrospect() public view returns (uint256) {\r\n        return token.balanceOf(address(this));\r\n    }\r\n\r\n    function myBalanceVariable() public view returns (uint256) {\r\n        return myTokenBalance;\r\n    }\r\n\r\n    function notAlwaysTrue() public view returns (bool) {\r\n        return myBalanceIntrospect() == myBalanceVariable();\r\n    }\r\n}\r\n```\r\n\r\n我们再次不能假设`myBalanceIntrospect()`和`myBalanceVariable()`总是返回相同的值。可以直接将ERC20代币转账到`MixedAccountingERC20`，绕过存款函数，不更新`myTokenBalance`变量。\r\n\r\n在用反省检查余额时，应避免严格使用相等检查，因为余额可以被外人随意改变。\r\n\r\n## 把加密证明当作密码一样对待\r\n\r\n这不是Solidity的一个怪癖，更多的是开发者对如何使用密码学来赋予地址特殊权限有普遍误解。下面的代码是不安全的：\r\n\r\n```solidity\r\ncontract InsecureMerkleRoot {\r\n    bytes32 merkleRoot;\r\n    function airdrop(bytes[] calldata proof, bytes32 leaf) external {\r\n\r\n        require(MerkleProof.verifyCalldata(proof, merkleRoot, leaf), \"not verified\");\r\n        require(!alreadyClaimed[leaf], \"already claimed airdrop\");\r\n        alreadyClaimed[leaf] = true;\r\n\r\n        mint(msg.sender, AIRDROP_AMOUNT);\r\n    }\r\n}\r\n```\r\n\r\n这段代码是不安全的，原因有三：\r\n\r\n1. 任何知道被选中进行空投的地址的人都可以重新创建Merkle树并创造一个有效的证明。\r\n2. 叶子没有Hash。攻击者可以提交一个与Merkle根相同的叶子，并绕过`require`语句。\r\n3. 即使上述两个问题被修复，一旦有人提交了有效的证明，他们就可以被抢跑。\r\n\r\n加密证明（Merkle树、签名等）需要与`msg.sender`绑定，攻击者在没有获得私钥的情况下无法操纵。\r\n\r\n## Solidity 不会向上转型 uint 大小\r\n\r\n```solidity\r\nfunction limitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) {\r\n    product = a * b;\r\n}\r\n```\r\n\r\n尽管product是一个[uint256](https://www.rareskills.io/post/uint-max-value-solidity)变量，但乘法结果不会大于255，否则代码将被回退。\r\n\r\n这个问题可以通过向上转型每个变量来解决：\r\n\r\n```solidity\r\nfunction unlimitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) {\r\n    product = uint256(a) * uint256(b);\r\n}\r\n```\r\n\r\n在结构中的整数相乘，也会出现这样的情况。当乘以在结构中的小数值时，你应该注意到这一点：\r\n\r\n```solidity\r\nstruct Packed {\r\n\tuint8 time;\r\n\tuint16 rewardRate\r\n}\r\n\r\n//...\r\n\r\nPacked p;\r\np.time * p.rewardRate; // this might revert!\r\n```\r\n\r\n\r\n\r\n## Solidity 截断不会回退\r\n\r\nSolidity 并不检查将一个整数转换为一个较小的整数是否安全。除非某些业务逻辑能确保向下转型是安全的，否则应该使用 [SafeCast](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol) 这样的库。\r\n\r\n```solidity\r\nfunction test(int256 value) public pure returns (int8) {\r\n\treturn int8(value + 1); // overflows and does not revert\r\n} \r\n```\r\n\r\n\r\n\r\n## 对存储指针的写入不会保存新数据\r\n\r\n这段代码看起来像是把myArray[1]中的数据复制到了myArray[0]中，但其实不是。如果你把函数的最后一行注释掉，编译器会说这个函数应该变成一个视图函数。对foo的写入并没有写到底层存储。\r\n\r\n```solidity\r\ncontract DoesNotWrite {\r\n    struct Foo {\r\n        uint256 bar;\r\n    }\r\n    Foo[] public myArray;\r\n\r\n    function moveToSlot0() external {\r\n        Foo storage foo = myArray[0];\r\n        foo = myArray[1]; // myArray[0] 不会改变\r\n        // we do this to make the function a state \r\n        // changing operation\r\n        // and silence the compiler warning\r\n        myArray[1] = Foo({bar: 100});\r\n    }\r\n}\r\n```\r\n\r\n所以不要写到存储指针。\r\n\r\n## 删除包含动态数据类型的结构体并不会删除动态数据\r\n\r\n如果一个映射（或动态数组）在一个结构体内，并且该结构被删除，那么映射或数组将不会被删除。\r\n\r\n除了删除数组之外，删除关键字只能删除一个存储槽。如果该存储槽包含对其他存储槽的引用，这些存储槽不会被删除。\r\n\r\n```solidity\r\ncontract NestedDelete {\r\n\r\n    mapping(uint256 => Foo) buzz;\r\n\r\n    struct Foo {\r\n        mapping(uint256 => uint256) bar;\r\n    }\r\n\r\n    Foo foo;\r\n\r\n    function addToFoo(uint256 i) external {\r\n        buzz[i].bar[5] = 6;\r\n    }\r\n\r\n    function getFromFoo(uint256 i) external view returns (uint256) {\r\n        return buzz[i].bar[5];\r\n    }\r\n\r\n    function deleteFoo(uint256 i) external {\r\n        // internal map still holds the data in the \r\n        // mapping and array\r\n        delete buzz[i];\r\n    }\r\n}\r\n```\r\n\r\n现在让我们做以下交易序列\r\n\r\n1. addToFoo(1)\r\n2. getFromFoo(1) 返回 6\r\n3. deleteFoo(1)\r\n4. getFromFoo(1) 仍然返回 6!\r\n\r\n记住，在Solidity中，map 永远不会是 \"空\"的。因此，如果有人访问一个已经被删除的项目，交易将不会回退，而是返回该数据类型的零值。\r\n\r\n---\r\n\r\n本翻译由 [DeCert.me](https://decert.me/) 协助支持， DeCert.me 的口号是`码一个未来`，支持每一位开发者构建自己的可信履历。"},"author":{"user":"https://learnblockchain.cn/people/412","address":null},"history":null,"timestamp":1684911088,"version":1}