diff --git a/05_DataStorage/readme.md b/05_DataStorage/readme.md index 0628f3bdf..a53a90e25 100644 --- a/05_DataStorage/readme.md +++ b/05_DataStorage/readme.md @@ -25,7 +25,7 @@ tags: ## 数据位置 -Solidity数据存储位置有三类:`storage`,`memory`和`calldata`。不同存储位置的`gas`成本不同。`storage`类型的数据存在链上,类似计算机的硬盘,消耗`gas`多;`memory`和`calldata`类型的临时存在内存里,消耗`gas`少。大致用法: +Solidity数据存储位置有三类:`storage`,`memory`和`calldata`。不同存储位置的`gas`成本不同。`storage`类型的数据存在链上,类似计算机的硬盘,消耗`gas`多;`memory`和`calldata`类型的临时存在内存里,消耗`gas`少。整体消耗`gas`从多到少依次为:`storage` > `memory` > `calldata`。大致用法: 1. `storage`:合约里的状态变量默认都是`storage`,存储在链上。 @@ -68,7 +68,7 @@ function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ ![5-2.png](./img/5-2.png) - `memory`赋值给`memory`,会创建引用,改变新变量会影响原变量。 -- 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方 +- 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方。这有时会涉及到开发中的问题,比如从`storage`中读取数据,赋值给`memory`,然后修改`memory`的数据,但如果没有将`memory`的数据赋值回`storage`,那么`storage`的数据是不会改变的。 ## 变量的作用域 diff --git a/12_Event/readme.md b/12_Event/readme.md index 39761e2fa..24ff435dc 100644 --- a/12_Event/readme.md +++ b/12_Event/readme.md @@ -80,6 +80,8 @@ keccak256("Transfer(address,address,uint256)") `indexed`标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 `indexed` 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。 +这里其实会引入一个新的问题,根据Solidity的[官方文档](https://docs.soliditylang.org/en/v0.8.27/abi-spec.html#encoding-of-indexed-event-parameters), 对于非值类型的参数(如arrays, bytes, strings), Solidity不会直接存储,而是会将`Keccak-256`哈希存储在主题中,从而导致数据信息的丢失。这对于某些依赖于链上事件的DAPP(跨链,用户注册等等)来说,可能会导致事件检索困难,需要解析哈希值。 + ### 数据 `data` 事件中不带 `indexed`的参数会被存储在 `data` 部分中,可以理解为事件的“值”。`data` 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 `data` 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 `topics` 部分中,也是以哈希的方式存储。另外,`data` 部分的变量在存储上消耗的gas相比于 `topics` 更少。 diff --git a/27_ABIEncode/readme.md b/27_ABIEncode/readme.md index 8fbf41c47..257234d3b 100644 --- a/27_ABIEncode/readme.md +++ b/27_ABIEncode/readme.md @@ -61,7 +61,7 @@ function encode() public view returns(bytes memory result) { ### `abi.encodePacked` -将给定参数根据其所需最低空间编码。它类似 `abi.encode`,但是会把其中填充的很多`0`省略。比如,只用1字节来编码`uint8`类型。当你想省空间,并且不与合约交互的时候,可以使用`abi.encodePacked`,例如算一些数据的`hash`时。 +将给定参数根据其所需最低空间编码。它类似 `abi.encode`,但是会把其中填充的很多`0`省略。比如,只用1字节来编码`uint8`类型。当你想省空间,并且不与合约交互的时候,可以使用`abi.encodePacked`,例如算一些数据的`hash`时。需要注意,`abi.encodePacked`因为不会做填充,所以不同的输入在拼接后可能会产生相同的编码结果,导致冲突,这也带来了潜在的安全风险。 ```solidity function encodePacked() public view returns(bytes memory result) { diff --git a/37_Signature/readme.md b/37_Signature/readme.md index 4f6fa1bd3..bba4ec547 100644 --- a/37_Signature/readme.md +++ b/37_Signature/readme.md @@ -137,7 +137,7 @@ print(f"签名:{signed_message['signature'].hex()}") 为了验证签名,验证者需要拥有`消息`,`签名`,和签名使用的`公钥`。我们能验证签名的原因是只有`私钥`的持有者才能够针对交易生成这样的签名,而别人不能。 -**4. 通过签名和消息恢复公钥:**`签名`是由数学算法生成的。这里我们使用的是`rsv签名`,`签名`中包含`r, s, v`三个值的信息。而后,我们可以通过`r, s, v`及`以太坊签名消息`来求得`公钥`。下面的`recoverSigner()`函数实现了上述步骤,它利用`以太坊签名消息 _msgHash`和`签名 _signature`恢复`公钥`(使用了简单的内联汇编): +**4. 通过签名和消息恢复公钥:**`签名`是由数学算法生成的。这里我们使用的是`rsv签名`,`签名`中包含`r, s, v`三个值的信息,长度分别为32 bytes,32 bytes,1 byte。而后,我们可以通过`r, s, v`及`以太坊签名消息`来求得`公钥`。下面的`recoverSigner()`函数实现了上述步骤,它利用`以太坊签名消息 _msgHash`和`签名 _signature`恢复`公钥`(使用了简单的内联汇编): ```solidity // @dev 从_msgHash和签名_signature中恢复signer地址 @@ -171,6 +171,9 @@ print(f"签名:{signed_message['signature'].hex()}") _msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b _signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c ``` + +需要注意的是,这里需要对输入参数`_signature`的长度进行检查,确保其长度为65bytes,否则会产生签名重放问题。具体问题可以参考[BlazCTF中的Cyber Cartel](https://github.com/DeFiHackLabs/blazctf-2024-writeup/blob/main/writeup/cyber-cartel.md). + ![通过签名和消息恢复公钥](./img/37-8.png) **5. 对比公钥并验证签名:** 接下来,我们只需要比对恢复的`公钥`与签名者公钥`_signer`是否相等:若相等,则签名有效;否则,签名无效: diff --git a/51_ERC4626/readme.md b/51_ERC4626/readme.md index a04c68737..60b66eb43 100644 --- a/51_ERC4626/readme.md +++ b/51_ERC4626/readme.md @@ -448,6 +448,8 @@ contract ERC4626 is ERC20, IERC4626 { } ``` +当然,本文中的`ERC4626`合约仅是为了教学演示使用,在实际使用时,还需要考虑如`Inflation Attack`, `Rounding Direction`等问题。在生产中,建议使用`openzeppelin`的具体实现。 + ## `Remix`演示 **注意:** 以下运行示例使用了remix中第二个账户,即`0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2`, 来部署合约, 调用合约方法. diff --git a/S01_ReentrancyAttack/readme.md b/S01_ReentrancyAttack/readme.md index 3f688cbc7..c1fd0cdd0 100644 --- a/S01_ReentrancyAttack/readme.md +++ b/S01_ReentrancyAttack/readme.md @@ -158,6 +158,8 @@ contract Attack { 4. 调用`Bank`合约的`getBalance()`函数,发现余额已被提空。 5. 调用`Attack`合约的`getBalance()`函数,可以看到余额变为`21 ETH`,重入攻击成功。 +当然,不仅仅`ETH`转账会触发重入攻击,`ERC721`和`ERC1155`的`safeTransfer()`和`safeTransferFrom()`安全转账函数,还有`ERC777`的`callback`函数,都可能会引发重入攻击。所以这更多的是一个宏观上的设计问题,而不仅仅局限于ETH转账本身。 + ## 预防办法 目前主要有两种办法来预防可能的重入攻击漏洞: 检查-影响-交互模式(checks-effect-interaction)和重入锁。 diff --git a/S06_SignatureReplay/readme.md b/S06_SignatureReplay/readme.md index e1fc0d77e..f989daed2 100644 --- a/S06_SignatureReplay/readme.md +++ b/S06_SignatureReplay/readme.md @@ -157,10 +157,21 @@ contract SigReplay is ERC20 { } ``` +3. 对于由用户输入`signature`的场景,需要检验`signature`的长度,确保其长度为`65bytes`,否则也会产生签名重放问题。 + + ```solidity + function mint(address to, uint amount, bytes memory signature) public { + require(signature.length == 65, "Invalid signature length"); + ... + } + ``` + ## 总结 -这一讲,我们介绍了智能合约中的签名重放漏洞,并介绍了两个预防方法: +这一讲,我们介绍了智能合约中的签名重放漏洞,并介绍了三个预防方法: 1. 将使用过的签名记录下来,防止二次使用。 -2. 将 `nonce` 和 `chainid` 包含到签名消息中。 \ No newline at end of file +2. 将 `nonce` 和 `chainid` 包含到签名消息中。 + +3. 对于由用户输入`signature`的场景,需要检验`signature`的长度,确保其长度为`65bytes`,否则也会产生签名重放问题。