{"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/5873)\r\n\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)，[第二篇](https://learnblockchain.cn/article/5860)， [第三篇](https://learnblockchain.cn/article/5867)\r\n\r\n\r\n## 权力过大的管理员\r\n\r\n仅仅因为一个合约有一个所有者或管理员，这并不意味着他们需要无限权力。考虑一个NFT。按理说，只有所有者才能从NFT销售中提取收益，但如果所有者的私钥被泄露，能够暂停合约（阻止转账）就会造成严重的破坏。一般来说，管理员的权限应该尽可能的小，以减少不必要的风险。\r\n\r\n说到合约所有权...\r\n\r\n## 使用Ownable2Step而不是Ownable\r\n\r\n这在技术上不是一个漏洞，但[OpenZeppelin ownable](https://www.rareskills.io/post/openzeppelin-ownable2step)如果所有权被转移到一个不存在的地址，会导致合约所有权的丧失。Ownable2step要求接收者确认所有权。这可以防止意外地将所有权发送到一个错误的地址。\r\n\r\n## 四舍五入的错误\r\n\r\nSolidity 没有浮点，所以舍入错误是不可避免的。设计者必须意识到正确的做法是向上舍入还是向下舍入，以及舍入应该对谁有利。\r\n\r\n除法应该总是最后进行。下面的代码在小数位数不同的稳定币之间进行了错误的转换。下面的兑换机制允许用户在兑换dai（精度为18）时免费获得少量的USDC（精度为6）。变量daiToTake将四舍五入为零，换取非零的usdcAmount时，用户拿不到任何东西。\r\n\r\n```solidity\r\ncontract Exchange {\r\n\r\n    uint256 private constant CONVERSION = 1e12;\r\n\r\n    function swapDAIForUSDC(uint256 usdcAmount) external pure returns (uint256 a) {\r\n        uint256 daiToTake = usdcAmount / CONVERSION;\r\n        conductSwap(daiToTake, usdcAmount);\r\n    }\r\n}\r\n```\r\n\r\n\r\n\r\n##  抢跑（Frontrunning）\r\n\r\n\r\n\r\n在 Etheruem（和类似的链）的背景下，Frontrunning 意味着观察一个待定的交易，并通过支付更高的 交易成本在它之前执行另一个交易。也就是说，攻击者已经 \"跑到了 \"交易的前面。如果该交易是一个有利可图的交易，那么除了支付更高的 交易成本，完全复制该交易是有意义的。\r\n\r\n这种现象有时被称为MEV，意思是矿工可提取的价值，但有时在其他情况下是最大可提取的价值。区块生产者有无限的权力来重新排序交易和插入自己的交易，从历史上看，在以太坊进入股权证明之前，区块生产者就是矿工，因此而得名。\r\n\r\n### 抢跑：不受限制的提款\r\n\r\n从智能合约中提取以太币可以被认为是一种 \"有利可图的交易\"。你执行了一个零成本的交易（除了Gas），最终拥有的加密货币比你开始时更多。\r\n\r\n```solidity\r\ncontract UnprotectedWithdraw {\r\n\r\n    constructor() payable {\r\n        require(msg.value == 1 ether, \"must create with 1 eth\");\r\n    }\r\n\r\n    function unsafeWithdraw() external {\r\n        (bool ok, ) = msg.sender.call{value: address(this).value}(\"\");\r\n        require(ok, \"transfer failed\").\r\n    }\r\n} \r\n```\r\n\r\n如果你部署了这个合约并试图退出，一个先行者机器人会注意到你在mempool中对 \"unsafeWithdraw \"的调用，并复制它来先获得以太币。\r\n\r\n### 抢跑：ERC4626 通膨攻击，是抢跑和四舍五入错误的组合\r\n\r\n我们已经在[ERC4626教程](https://www.rareskills.io/post/erc4626)中深入介绍了ERC-4626 的通膨攻击。但它的要点是，ERC4626 合约根据交易者贡献的 \"资产 \"的百分比来分配 \"份额\"代币。大致上，它的运作方式如下：\r\n\r\n```solidity\r\nfunction getShares(...) external {\r\n    // code\r\n    shares_received = assets_contributed / total_assets;\r\n    // more code\r\n}\r\n```\r\n\r\n当然，没有人会贡献资产而得不到任何股份，但他们无法预测这种情况会发生，如果有人能在交易中先发制人，获得股份。\r\n\r\n例如，当池子里有20个资产时，他们贡献了200个资产，他们期望得到100 份额。但是，如果有人在交易中提前存入200个资产，那么公式将是200/220，四舍五入为零，导致受害者失去资产，获得零份额。\r\n\r\n### 抢跑：ERC20授权\r\n\r\n我用一个真实的例子来说明这一点，而不是抽象地描述它：\r\n\r\n1. 假设 Alice 授权了Eve的100个代币。Eve 是邪恶的代表，而不是用 Bob ，所以我们将保持惯例。\r\n2. Alice 改变了主意，发送了一个交易，将 Ev e的授权改为50。\r\n3. 在将授权额度改为50的交易纳入区块之前，它位于Mempool中，Eve可以看到它。\r\n4. Eve 发送了一个交易，要求获得她的100个代币，这在将授权改为50之前。\r\n5. 对 50 的授权的交易通过了。\r\n6. Eve 又获取了 50 个代币。\r\n\r\n现在 Eve 有150个代币，而不是100或50。解决这个问题的办法是，在处理不受信任的授权时，在增加或减少授权之前，将授权设置为零。\r\n\r\n### 抢跑：三明治攻击\r\n\r\n一项资产的价格会随着买卖压力的变化而变化。如果一个大订单在Mempool中，交易者有动力去复制这个订单，但要有更高的gas成本。这样一来，他们就会购买资产，让用户的大额订单使价格上涨，然后他们马上卖出。卖出订单有时被称为 \"尾随\"。卖出订单可以通过放置一个较低gas成本的卖出订单来完成，这样的序列看起来像这样的\r\n\r\n1. 抢跑买入\r\n2. 大额买入（用户）\r\n3. 卖出\r\n\r\n对这种攻击的主要防御是提供一个 \"滑点\"参数。如果 \"抢跑买入 \"本身将价格推高到某个阈值以上，\"大额买入\"订单将回退，使抢跑者的交易失败。\r\n\r\n这就是所谓的三明治攻击（sandwhich），因为大额买入被抢跑买入和尾随卖出夹在中间。这种攻击也适用于大额卖单，只是方向相反。\r\n\r\n### 了解更多关于抢跑的信息\r\n\r\n抢跑是一个巨大的话题。[Flashbots](https://www.flashbots.net/)已经对这个话题进行了广泛的研究，并发表了一些工具和研究文章，以帮助最大限度地减少它的负面外部因素。通过适当的区块链架构是否可以 \"设计掉 \"抢跑是一个争论不休的话题，还没有得到最终的解决。以下两篇文章是关于这个问题的永恒的经典之作：\r\n\r\n[以太坊是一个黑暗的森林](https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest)\r\n\r\n[逃离黑暗森林](https://samczsun.com/escaping-the-dark-forest/)\r\n\r\n## 签名相关\r\n\r\n数字签名在智能合约的背景下有两种用途：\r\n\r\n- 使得地址能够授权区块链上的一些交易，而不进行实际交易\r\n- 根据预定的地址，向智能合约证明发起者有某种权力去做某事\r\n\r\n下面是一个安全使用数字签名的例子，让用户拥有铸造NFT的特权：\r\n\r\n```solidity\r\nimport \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\r\nimport \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\r\n\r\ncontract NFT is ERC721(\"name\", \"symbol\") {\r\n    function mint(bytes calldata signature) external {\r\n        address recovered = keccak256(abi.encode(msg.sender)).toEthSignedMessageHash().recover(signature);\r\n        require(recovered == authorizer, \"signature does not match\");\r\n    }\r\n}\r\n```\r\n\r\n一个典型的例子是ERC20中的`Approve`函数。为了授权一个地址从我们的账户中提取一定数量的代币，我们必须进行实际的以太坊交易，这需要花费Gas。\r\n\r\n\r\n\r\n有时，将数字签名传递给链外的接收者，然后接收者向智能合约提供签名，以证明他们被授权进行交易，这样做更有效率。\r\n\r\nERC20Permit实现了用数字签名进行审批。该函数描述如下:\r\n\r\n```solidity\r\nfunction permit(address owner,\r\n    address spender,\r\n    uint256 amount,\r\n    uint256 deadline,\r\n    uint8 v,\r\n    bytes32 r,\r\n    bytes32 s\r\n) public\r\n```\r\n\r\n而不是发送一个实际的授权交易，所有者可以为花费者 \"签署\"授权（连同一个截止日期）。然后，被授权的花费者可以用提供的签署信息作为参数调用permint函数。\r\n\r\n### 签名的剖析\r\n\r\n你会经常看到v、r和s这些变量。它们在solidity中分别用uint8、bytes32和bytes32的数据类型表示。有时，签名被表示为一个65字节的数组，它是所有这些值连接起来的，如`abi.encodePacked(r, s, v)`；\r\n\r\n签名的另外两个基本组成部分是消息哈希值（32字节）和签名地址。这个序列看起来像这样\r\n\r\n1. 私钥（privKey）用于生成一个公共地址（ethAddress）。\r\n2. 一个智能合约预先存储地址 ethAddress\r\n3. 一个链外用户对一个消息进行Hash，并对Hash值进行签名。这就产生了一对msgHash和签名（r, s, v）。\r\n4. 智能合约收到一条消息，对其进行散列以产生msgHash，然后将其与(r, s, v)相结合，看得出什么地址。\r\n5. 如果地址与ethAddress匹配，则签名有效（在某些假设下，我们很快就会看到！）。\r\n\r\n智能合约使用步骤4中的[预编译合约](https://www.rareskills.io/post/solidity-precompiles) ecrecover 来完成我们所说的组合，并获得地址。\r\n\r\n在这个过程中，有很多步骤都会出现问题。\r\n\r\n### 签名：ecrecover返回地址(0)，当地址无效时，不会被回退\r\n\r\n如果一个未初始化的变量与ecrecover的输出相比较，这可能导致漏洞。\r\n\r\n这段代码有漏洞：\r\n\r\n```solidity\r\ncontract InsecureContract {\r\n\r\n    address signer; \r\n    // defaults to address(0)\r\n    // who lets us give the beneficiary the airdrop without them// spending gas\r\n    function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {\r\n\r\n        // 如果签名无效，ecrecover 会返回 address(0) \r\n        require(signer == ecrecover(keccak256(abi.encode(who, amount)), v, r, s), \"invalid signature\");\r\n\r\n        mint(msg.sender, AIRDROP_AMOUNT);\r\n    }\r\n}\r\n```\r\n\r\n### 签名重放\r\n\r\n签名重放发生在合约没有跟踪签名是否先前被使用。在下面的代码中，我们修复了之前的问题，但它仍然不安全。\r\n\r\n```solidity\r\ncontract InsecureContract {\r\n\r\n    address signer;\r\n\r\n    function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {\r\n\r\n        address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s);\r\n        require(recovered != address(0), \"invalid signature\");\r\n        require(recovered == signer, \"recovered signature not equal signer\");\r\n\r\n\r\n        mint(msg.sender, amount);\r\n    }\r\n}\r\n```\r\n\r\n人们可以随心所欲地索取空投!\r\n\r\n我们可以添加以下几行：\r\n\r\n```solidity\r\nbytes memory signature = abi.encodePacked(v, r, s);\r\nrequire(!used[signature], \"signature already used\"); \r\n// mapping(bytes => bool);\r\nused[signature] = true;\r\n```\r\n\r\n唉，这段代码还是不安全啊!\r\n\r\n### 签名的可塑性\r\n\r\n给定一个有效的签名，攻击者可以做一些快速的算术来推导出一个不同的签名。然后，攻击者可以 \"重放\"这个修改过的签名。但首先，让我们提供一些代码，证明我们可以从一个有效的签名开始，修改它，并显示新的签名仍然通过。\r\n\r\n```solidity\r\ncontract Malleable {\r\n\r\n    // v = 28\r\n    // r = 0xf8479d94c011613baeffe9239e4ff65e2adbac744c34217ca7d51378e72c5204\r\n    // s = 0x57af17590a914b759c45aaeabaf513d5ef72d7da1bdd19d9f2e1bc371ece5b86\r\n    // m = 0x0000000000000000000000000000000000000000000000000000000000000003\r\n    function foo(bytes calldata msg, uint8 v, bytes32 r, bytes32 s) public pure returns (address, address){\r\n        bytes32 h = keccak256(msg);\r\n        address a = ecrecover(h, v, r, s);\r\n\r\n\r\n        // 下面是一个数据魔法用来反转签名并创建有效的签名\r\n        // flip s\r\n        bytes32 s2 = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint256(s));\r\n\r\n        // invert v\r\n        uint8 v2;\r\n        require(v == 27 || v == 28, \"invalid v\");\r\n        v2 = v == 27 ? 28 : 27;\r\n\r\n        address b = ecrecover(h, v2, r, s2);\r\n\r\n        assert(a == b); \r\n        // different signatures, same address!;\r\n        return (a, b);\r\n    }\r\n}\r\n```\r\n\r\n因此，我们的运行实例仍然是脆弱的。一旦有人提出一个有效的签名，就可以产生镜像签名并绕过使用的签名检查。\r\n\r\n```solidity\r\ncontract InsecureContract {\r\n\r\n    address signer;\r\n\r\n    function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {\r\n\r\n        address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s);\r\n        require(recovered != address(0), \"invalid signature\");\r\n        require(recovered == signer, \"recovered signature not equal signer\");\r\n\r\n        bytes memory signature = abi.encodePacked(v, r, s);\r\n        require(!used[signature], \"signature already used\"); // this can be bypassed\r\n        used[signature] = true;\r\n\r\n        mint(msg.sender, amount);\r\n    }\r\n}\r\n```\r\n\r\n### 安全签名\r\n\r\n在这一点上，你可能想得到一些安全的签名代码，对吗？我们向你推荐我们的[在solidity中创建签名](https://www.rareskills.io/post/openzeppelin-verify-signature)和在foundry中测试签名的教程。但这里是检查清单：\r\n\r\n- 使用openzeppelin的库来防止可塑性攻击，并还原到零地址的问题\r\n- 不要使用签名作为密码。信息需要包含攻击者不能轻易重复使用的信息（如msg.sender）。\r\n- 在链上对你所签署的内容进行Hash\r\n- 使用 `nonce` 来防止重放攻击。更好的是，遵循EIP712，这样用户可以看到他们正在签署的内容，并且可以防止签名在合约和不同链之间被重复使用。\r\n\r\n### 签名在没有适当的保障措施的情况下可以被伪造或篡改\r\n\r\n如果没有在链上进行Hash，上面的攻击可以进一步泛化。在上面的例子中，Hash是在智能合约中完成的，所以上面的例子不容易被下面的攻击所攻击。\r\n\r\n让我们来看看还原签名的代码：\r\n\r\n```solidity\r\n// 这个代码是不安全的\r\nfunction recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public returns (address signer) {\r\n    require(signer == ecrecover(hash, v, r, s), \"signer does not match\");\r\n    // more actions\r\n}\r\n```\r\n\r\n用户同时提供哈希值和签名。如果攻击者已经从签名者那里看到了一个有效的签名，他们可以简单地重复使用另一个消息的哈希和签名。\r\n\r\n这就是为什么在智能合约中对消息进行哈希非常重要，而不是在链外。\r\n\r\n要看这个漏洞的操作，请看我们在Twitter上发布的CTF。\r\n\r\n原始挑战：\r\n\r\nPart 1: https://twitter.com/RareSkills_io/status/1650869999266037760\r\n\r\nPart 2: https://twitter.com/RareSkills_io/status/1650897671543197701\r\n\r\n解决方案：\r\n\r\nhttps://twitter.com/RareSkills_io/status/1651527648676573185 https://twitter.com/RareSkills_io/status/1651224817465540611\r\n\r\n### 签名作为标识符\r\n\r\n签名不应该被用来识别用户。由于可塑性，它们不能被认为是唯一的。Msg.sender有更强的唯一性保证。\r\n\r\n## 某些 Solidity 编译器版本有错误\r\n\r\n\r\n\r\n请看我们在 Twitter 上发布的安全演练，见[这里](https://twitter.com/RareSkills_io/status/1648295439249317889)。当审核一个代码库时，对照 Solidity 页面上的 [发布公告](https://blog.soliditylang.org/category/releases/) 检查 Solidity 版本，以查看是否可能存在一个错误。\r\n\r\n## 假设智能合约是不可变的\r\n\r\n智能合约可以用代理模式（或更少的情况下，metamorphic 模式）进行升级。智能合约不应该依赖任意智能合约的函数保持不变。\r\n\r\n## Transfer()和send()在多签名钱包中会出现问题\r\n\r\nsolidity函数transfer和send不应该被使用。它们有意将随交易转发的Gas量限制在2300，这将导致大多数操作的Gas量耗尽。\r\n\r\n常用的gnosisSafe多签名钱包支持在[回退函数](https://github.com/safe-global/safe-contracts/blob/cb4b2b19b3e336b8defd3b8c9e0e6a2ae130598c/contracts/base/FallbackManager.sol#L61)中转发调用到另一个地址。如果有人使用transfer或send向多签名钱包发送以太币，那么回退函数可能会耗尽Gas，转账会失败。下面是gnosisSafe回退函数的截图。读者可以清楚地看到有足够多的操作可以用完2300个Gas。\r\n\r\n![gnosisSafe回退函数](https://img.learnblockchain.cn/pics/20230526160245.png)\r\n\r\n\r\n\r\n如果你需要与使用 transfer 和 send 的合约进行交互，请参阅我们关于[以太坊 access list 交易](https://www.rareskills.io/post/eip-2930-optional-access-list-ethereum)的文章，该文章允许你减少存储和合约访问操作的Gas成本。\r\n\r\n## 算术溢出检测还有意义吗？\r\n\r\nSolidity 0.8.0已经内置了溢出和下溢保护。因此，除非存在`unchecked` 的块，或使用Yul中的低级代码，否则就不会有溢出的危险。因此，不应该使用SafeMath库，因为它们在额外的检查上浪费Gas。\r\n\r\n## block.timestamp 怎么样？\r\n\r\n一些文档指出，block.timestamp是一个漏洞，因为矿工可以操纵它。这通常用于使用时间戳作为随机数的来源，正如前面记载的那样，无论如何都不应该这样做。合并后的以太坊以精确的12秒（或12秒的倍数）间隔更新时间戳。然而，以秒为粒度测量时间是一种反模式的做法。在一分钟的范围内，如果验证者错过了他们的区块slot，并且在区块生产中发生了24秒的差距，就会有相当多的错误机会。\r\n\r\n## 边缘案例\r\n\r\n边缘案例不容易定义，但一旦你看到了足够多的边缘案例，你就会开始对它们形成一种直觉。边缘案例可以是像有人试图索取奖励，但却没有任何抵押品的情况。这是有效的，我们应该给他们零奖励。同样，我们通常希望平均分配奖励，但如果只有一个接收者，技术上不应该有除法呢？\r\n\r\n### 边缘案例：实例1\r\n\r\n这个例子取自Akshay Srivastav的[twitter thread](https://twitter.com/akshaysrivastv/status/1648310441058115592)，并进行了修改。\r\n\r\n考虑这样的情况：如果一组特权地址为其提供了签名，那么某人就可以进行一个特权动作：\r\n\r\n```solidity\r\ncontract VulnerableMultisigAuthorization {\r\n    struct Authorization {\r\n        bytes signature;\r\n        address authorizer;\r\n        bytes32 hashOfAction;\r\n        // more fields\r\n    }\r\n\r\n    // more code\r\n    function takeAction(Authorization[] calldata auths, bytes calldata action) public {\r\n        // logic for avoiding replay attacks\r\n        for (uint256 i; i < auths.length; ++i) {\r\n\r\n            require(validateSignature(auths[i].signature, auths[i].authorizer), \"invalid signature\");\r\n            require(authorizers[auths[i].authorizer], \"address is not an authorizer\");\r\n\r\n        }\r\n\r\n        doTheAction(action)\r\n    }\r\n}\r\n```\r\n\r\n如果任何一个签名是无效的，或者签名与有效的地址不匹配，就会发生还原。但是如果数组是空的呢？在此案例中，它将直接跳到`doTheAction`而不需要任何签名。\r\n\r\n### 边缘案例：例子2\r\n\r\n```solidity\r\ncontract ProportionalRewards {\r\n\r\n    mapping(address => uint256) originalId;\r\n    address[] stakers;\r\n\r\n    function stake(uint256 id) public {\r\n        nft.transferFrom(msg.sender, address(this), id);\r\n        stakers.append(msg.sender);\r\n    }\r\n\r\n    function unstake(uint256 id) public {\r\n        require(originalId[id] == msg.sender, \"not the owner\");\r\n\r\n        removeFromArray(msg.sender, stakers);\r\n\r\n        sendRewards(msg.sender, \r\n            totalRewardsSinceLastclaim() / stakers.length());\r\n\r\n        nft.transferFrom(address(this), msg.sender, id);\r\n    }\r\n}\r\n```\r\n\r\n虽然上面的代码没有显示所有的函数实现，但即使这些函数的行为和它们的名字描述的一样，仍然有一个错误。你能发现它吗？这里有一张图片，给你一些空间，在你向下滚动之前，不要看到答案。\r\n\r\n![image-20230526161438312](https://img.learnblockchain.cn/pics/20230526161439.png)\r\n\r\n\r\n\r\n`removeFromArray`和`sendRewards`函数的顺序是错误的。如果stakers数组中只有一个用户，就会出现除以0的错误，而用户将无法提取他们的NFT。此外，奖励可能没有按照作者的意图来划分。如果原来有4个stakers，一个人退出，他将得到三分之一的奖励，因为在退出时数组的长度是3。\r\n\r\n### 边缘案例3：Compound 奖励计算错误\r\n\r\n让我们用一个真实的例子来说明，根据一些估计，这个例子造成了超过1亿美元的损失。如果你不完全理解Compound协议，不要担心，我们将只关注相关部分。(另外，Compound 协议是DeFi历史上最重要和最有影响的协议之一\r\n\r\n> 在 [区块链技术集训营](https://learnblockchain.cn/course/28)  有 Compound 协议的讲解\r\n\r\n> \r\n\r\n总之，Compound的重点是奖励用户将其闲置的加密货币借给其他可能有用途的交易者。贷款人以利息和COMP代币的形式获得报酬（借款人可以要求获得COMP代币奖励，但我们现在不会关注这个问题）。\r\n\r\nCompound Comptroller是一个代理合约，它将调用委托给可由Compound治理机构设置的实现。\r\n\r\n在2021年9月30日的治理[提案62](https://compound.finance/governance/proposals/62)中，实施合约被设置为一个有漏洞的[实现合约](https://etherscan.io/address/0x374abb8ce19a73f2c4efad642bda76c797f19233/advanced#internaltx)。在上线的同一天，人们在[Twitter](https://twitter.com/napgener/status/1443350694635921409?ref_src=twsrc^tfw|twcamp^tweetembed|twterm^1443350694635921409|twgr^16d6caea3da2d69f5fad63b233d4b0848bb558c6|twcon^s1_&ref_url=https%3A%2F%2Fwww.coindesk.com%2Ftech%2F2021%2F09%2F30%2Fdefi-money-market-compound-overpays-15m-in-comp-rewards-in-possible-exploit%2F)上看到，一些交易在押注零代币的情况下，却能申领 COMP 奖励。\r\n\r\n有漏洞的函数 `distributeSupplierComp()`\r\n\r\n以下是原始代码：\r\n\r\n```solidity\r\n/**\r\n * @notice Calculate COMP accrued by a supplier and possibly transfer it to them\r\n * @param cToken The market in which the supplier is interacting\r\n * @param supplier The address of the supplier to distribute COMP to\r\n */\r\nfunction distributeSupplierComp(address cToken, address supplier) internal {\r\n    // TODO: Don't distribute supplier COMP if the user is not in the supplier market.\r\n    // This check should be as gas efficient as possible as distributeSupplierComp is called in many places.\r\n    // - We really don't want to call an external contract as that's quite expensive.\r\n\r\n    CompMarketState storage supplyState = compSupplyState[cToken];\r\n    uint supplyIndex = supplyState.index;\r\n    uint supplierIndex = compSupplierIndex[cToken][supplier];\r\n\r\n    // Update supplier's index to the current index since we are distributing accrued COMP\r\n    compSupplierIndex[cToken][supplier] = supplyIndex;\r\n\r\n    if (supplierIndex == 0 && supplyIndex > compInitialIndex) {\r\n        // Covers the case where users supplied tokens before the market's supply state index was set.\r\n        // Rewards the user with COMP accrued from the start of when supplier rewards were first\r\n        // set for the market.\r\n        supplierIndex = compInitialIndex;\r\n    }\r\n\r\n    // Calculate change in the cumulative sum of the COMP per cToken accrued\r\n    Double memory deltaIndex = Double({mantissa: sub_(supplyIndex, supplierIndex)});\r\n\r\n    uint supplierTokens = CToken(cToken).balanceOf(supplier);\r\n\r\n    // Calculate COMP accrued: cTokenAmount * accruedPerCToken\r\n    uint supplierDelta = mul_(supplierTokens, deltaIndex);\r\n\r\n    uint supplierAccrued = add_(compAccrued[supplier], supplierDelta);\r\n    compAccrued[supplier] = supplierAccrued;\r\n\r\n    emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex);\r\n}\r\n```\r\n\r\n具有讽刺意味的是，这个漏洞在TODO的注释中。\"如果用户不在供应市场，就不要分发供应者COMP\"。但代码中没有这方面的检查。而只要用户在他们的钱包中持有份额代币（CToken(cToken).balanceOf(supplier);）\r\n\r\n在[提案64](https://compound.finance/governance/proposals/64)在2021年10月9日修复了这个错误。\r\n\r\n虽然这可以说是一个输入验证的错误，但用户并没有提交任何恶意的参数。如果有人试图申领奖励而没有stake任何东西，正确的计算结果应该是零。可以说，这更像是一个商业逻辑或边缘案例错误。\r\n\r\n## 真实世界的黑客\r\n\r\n在现实世界中发生的DeFi黑客，很多时候并不属于上述的确定好的类别。\r\n\r\n### Pairity钱包冻结(2017年11月)\r\n\r\nparity钱包并不打算直接使用。它是一个参考实现，[智能合约最小克隆实现](https://learnblockchain.cn/article/721)会指向它。如果需要的话，实现允许克隆者自毁，但这需要所有的钱包所有者都签字同意。\r\n\r\n```solidity\r\n// throw unless the contract is not yet initialized.modifier \r\n\r\nonly_uninitialized { if (m_numOwners > 0) throw; _; }\r\n\r\nfunction initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {\r\n  initDaylimit(_daylimit);\r\n  initMultiowned(_owners, _required);\r\n}\r\n```\r\n\r\n钱包所有者被声明：\r\n\r\n```solidity\r\n// kills the contract sending everything to `_to`.\r\nfunction kill(address _to) onlymanyowners(sha3(msg.data)) external {\r\n  suicide(_to);\r\n}\r\n```\r\n\r\n一些文献将此描述为 \"不受保护的自毁\"，即访问控制失败，但这并不十分准确。问题是initWallet函数没有在实现合约上被调用，这就允许有人自己调用initWallet函数，使自己成为所有者。这使他们有权力调用kill函数。根本原因是，实现没有被初始化。因此，这个错误不是由于solidity代码有问题而引起的，而是由于部署过程有问题。\r\n\r\n### Badger DAO Hack (2021年12月)\r\n\r\n在这个黑客攻击中没有利用Solidity代码。相反，攻击者获得了Cloudflare的API密钥，并在网站前端注入了一个脚本，改变了用户的交易，将提款引向攻击者的地址。阅读更多内容，请点击此[文章](https://www.theverge.com/2021/12/2/22814849/badgerdao-defi-120-million-hack-bitcoin-ethereum)。\r\n\r\n## 钱包的攻击载体\r\n\r\n### 随机数不足的私钥\r\n\r\n发现有很多前导零的地址的动机是，它们使用起来更节省Gas。以太坊交易数据中的一个零字节被收取4个Gas，一个非零字节被收取16个Gas。因此、\r\n\r\nWintermute被黑了，因为它使用了亵渎的地址（[writeup](https://www.halborn.com/blog/post/explained-the-wintermute-hack-september-2022)）。下面是1inch的[写法](https://blog.1inch.io/a-vulnerability-disclosed-in-profanity-an-ethereum-vanity-address-tool/)，说明亵渎地址发生器是如何被破坏的。\r\n\r\n信任钱包有一个类似的漏洞，在[这篇文章](https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been-stolen/)中记录了漏洞情况。\r\n\r\n请注意，这并不适用于通过改变create2中的盐而发现的带有前导零的智能合约，因为智能合约没有私钥。\r\n\r\n### 重复使用nonce或nonce不够随机\r\n\r\n椭圆曲线签名上的 \"r \"和 \"s \"点的生成方法如下：\r\n\r\n```\r\nr = k * G (mod N)\r\ns = k^-1 * (h + r * privateKey) (mod N)\r\n```\r\n\r\n\r\n\r\nG，r，s，h，和N都是公开的。如果 \"k\"变得公开，那么 \"privateKey \"就是唯一的未知变量，并且可以被算出。正因为如此，钱包需要完全随机地生成k，并且永远不会重复使用它。如果随机数不是完全随机的，那么k就可以被推断出来。2013年，Java库中不安全的随机数生成让很多安卓的比特币钱包受到攻击。(比特币使用与以太坊相同的签名算法。）（https://arstechnica.com/information-technology/2013/08/all-android-created-bitcoin-wallets-vulnerable-to-theft/）。\r\n\r\n## 大多数漏洞都是特定的应用发生\r\n\r\n训练自己快速识别这个列表中的反模式将使你成为一个更有效的智能合约程序员，但大多数智能合约漏洞的后果是由于预期的商业逻辑和代码的实际操作之间的不匹配。\r\n\r\n其他可能发生bug的领域：\r\n\r\n- 糟糕的 Token 激励\r\n- 边界判断的错误\r\n- 拼写错误\r\n- 管理员或用户的私钥被盗\r\n\r\n## 许多漏洞本可以通过单元测试被发现\r\n\r\n[智能合约单元测试](https://www.rareskills.io/post/foundry-testing-solidity)可以说是智能合约最基本的保障措施，但数量惊人的智能合约要么缺乏单元测试，要么[测试覆盖率](https://www.rareskills.io/post/foundry-forge-coverage)不足。\r\n\r\n但单元测试往往只测试合约的 \"快乐路径\"（预期/设计的行为）。为了测试那些令人惊讶的情况，必须采用额外的测试方法。\r\n\r\n在智能合约被送去审计之前，应该先做以下工作：\r\n\r\n- 用Slither等工具进行静态分析，确保不遗漏基本错误\r\n- 通过单元测试实现100%的行和分支覆盖\r\n- 突变（Mutation）测试，确保单元测试有健全的断言语句\r\n- 模糊测试，特别是算术测试\r\n- 对有状态的属性进行不变量测试\r\n- 适当时进行形式化验证\r\n\r\n对于那些不熟悉这里的一些方法的人，Cyfrin Audits的Patrick Collins在他的[视频](https://www.youtube.com/watch?v=juyY-CTolac)中对有状态和无状态的模糊测试做了幽默的介绍。\r\n\r\n完成这些任务的工具正迅速变得更加广泛和容易使用。\r\n\r\n## 更多资源\r\n\r\n一些作者在这些Repos中汇编了一份以前的DeFi黑客的清单：\r\n\r\n- https://github.com/coinspect/learn-evm-attacks\r\n- https://github.com/SunWeb3Sec/DeFiHackLabs\r\n- https://rekt.news/\r\n\r\nSecureum已被广泛用于研究和实践安全问题，但请记住，该版本已经两年没有实质性的更新了。\r\n\r\n- https://github.com/x676f64/secureum-mind_map\r\n\r\n你可以通过我们的[Solidity Riddles](https://github.com/RareSkills/solidity-riddles)资源库练习利用solidity的漏洞。\r\n\r\n- https://github.com/RareSkills/solidity-riddles\r\n\r\nDamnVulnerableDeFi是每个开发者都应该练习的经典战争游戏\r\n\r\n- [https://damnvulnerabledefi.xyz](https://damnvulnerabledefi.xyz/)\r\n\r\nCapture The Ether和Ethernaut是经典之作，但请记住，有些问题是过于简单，或者教授过时的Solidity概念。\r\n\r\n- [https://capturetheether.com](https://capturetheether.com/)\r\n- [https://ethernaut.openzeppelin.com](https://ethernaut.openzeppelin.com/)\r\n\r\n一些有信誉的众包安全公司有一个有用的过去的审计清单可以研究。\r\n\r\n- https://code4rena.com/\r\n- https://www.sherlock.xyz/\r\n\r\n## 成为智能合约审计师\r\n\r\n如果你不精通Solidity，那么你就没有办法审计以太坊智能合约。如果你刚刚开始，请看我们的[免费Solidity教程](https://decert.me/tutorial/solidity/intro/)。\r\n\r\n成为智能合约审计师并没有行业认可的认证。任何人都可以创建一个网站和社交媒体资料，声称自己是solidity审计师，并开始销售服务，而且很多人已经这样做了。因此，在雇用之前要谨慎行事，并获得推荐。\r\n\r\n要成为智能合约审计师，你需要在发现错误方面大大优于普通的solidity开发者。因此，成为审计师的 \"路线图 \"无非是几个月的不懈努力和刻意练习，直到你比大多数人更会捕捉智能合约的错误。\r\n\r\n如果你在识别漏洞方面缺乏超越同行的决心，那么你就不可能在训练有素、积极进取的犯罪分子之前发现关键问题。\r\n\r\n### 关于你成为智能合约安全审计师的成功机会的冷酷事实\r\n\r\n最近，智能合约审计被认为是一个理想的工作领域，因为人们认为它是有利可图的。的确，一些漏洞赏金的支付已经超过了100万美元，但这是极为罕见的例外，而不是常态。\r\n\r\n\r\n\r\nCode4rena有一个公开的[排行榜](https://code4rena.com/leaderboard/)，列出了竞争对手在其审计竞赛中的报酬，这为我们提供了一些关于成功率的数据。\r\n\r\n排行榜上有1171个名字，但是\r\n\r\n- 只有29名竞争者的全部收入超过10万美元（2.4%）。\r\n- 只有57人全部的收入超过5万美元（4.9%）。\r\n- 只有170人的全部收入超过1万美元（14.5%）。\r\n\r\n还要考虑到这一点，当Openzeppelin开放安全研究奖学金（不是工作，是工作前的筛选和培训）的申请时，他们收到了超过300份申请，但只选择了不到10名候选人，其中能得到全职工作的人更少。\r\n\r\n![OpenZeppelin智能合约审核员工作申请](https://img.learnblockchain.cn/pics/20230523144858.png)\r\n\r\n>  https://twitter.com/David_Bessin/status/1625167906328944640\r\n\r\n这比哈佛大学的录取率还低。\r\n\r\n智能合约审计是一个竞争性的零和游戏。只有这么多的项目需要审计，只有这么多的预算用于安全，只有这么多的bug需要发现。如果你现在开始研究安全问题，有几十个积极性很高的个人和团队会比你有巨大的领先优势。大多数项目都愿意为有声誉的审计师而不是未经测试的新审计师支付溢价。\r\n\r\n在这个安全系列中，我们列出了至少20种不同类别的漏洞。如果你花了一周的时间来掌握每一种（这有点乐观），你只是刚刚开始了解对有经验的审计师来说是常识的东西。在这个安全系列中，我们还没有涉及Gas优化和经济模型，这两个都是审计师需要了解的重要课题。算一算，你会发现这不是一个短暂的旅程。\r\n\r\n对于那些阅读这个安全系列并希望在智能合约安全方面有所作为的人来说，重要的是要清楚地了解，获得有利可图的职业的几率并不对你有利。成功并不是默认的结果。\r\n\r\n当然，这也是可以做到的，有不少人从对Solidity一无所知到在审计领域拥有一个有利可图的职业。可以说，在两年的时间内找到一份智能合约审计师的工作，比进入法学院并通过律师考试要容易。与其他很多职业选择相比，它当然有更多的上升空间。\r\n\r\n但是，这仍然需要你有巨大的毅力来掌握你面前的快速发展的知识山，并磨练你发现错误的直觉。\r\n\r\n这并不是说学习智能合约安全不是一个值得的追求。它绝对是。但需要合理的期待。\r\n\r\n## 结语\r\n\r\n了解已知的反模式是很重要的。然而，现实世界中的大多数漏洞都是特定的应用。识别这两类漏洞都需要持续不断的刻意练习。\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":1685091540,"version":1}