{"content":{"title":"生动理解call方法与delegatecall方法","body":"在了解两个方法前，需要提到一个概念: ABI\r\n\r\n## 什么是ABI\r\n\r\nABI ：根据英文  “Application  Binary  Interface”翻译过来就是，应用程序二进制接口\r\n\r\n### ABI的作用\r\n\r\n应用程序可以与智能合约进行交互，比如发送交易、查询状态、获取事件。在链上，我们所有的数据都是以字节码的形式存在，所以当我们调用某一个合约，相当于对该合约发送一个交易，其中交易的内容，传输的数据，指令，就是预先进行了abi编码进行传输的，之后匹配合约的字节码，进行调用。\r\n\r\n### ABI编码\r\n\r\n函数选择器：对函数签名，去前面四个字节，使用  Keccak-256加密哈希函数进行计算得到结果\r\n\r\n## call方法\r\n\r\n### 特点\r\n\r\n1. 返回一个元组，包含两个值，一个是布尔值，表示交易成功或失败，另一个是字节(bytes)，其字节是执行函数的返回值经过了abi编码(如果执行函数无返回值则返回为空)\r\n\r\n2. 没有`gas`限制，可以支持对方合约`fallback()`或`receive()`函数实现复杂逻辑\r\n3. 转账失败，不会`revert`，所以通常需要与 revert 函数一同使用，revert会撤销自事务开始以来对区块链状态的所有更改\r\n\r\n### 演示\r\n\r\n我们来看看实例，就以数字增加的简单代码来演示\r\n\r\n1. 被 called 的代码\r\n\r\n   ```solidity\r\n   // SPDX-License-Identifier: MIT\r\n   pragma solidity ^0.8.0;\r\n   \r\n   contract called {\r\n       uint8 public num = 1;\r\n       function count() public returns (uint8) {\r\n           num++;\r\n           return num;\r\n       }\r\n   }\r\n   ```\r\n\r\n2. caller的代码\r\n\r\n   ```solidity\r\n   // SPDX-License-Identifier: MIT\r\n   pragma solidity ^0.8.0;\r\n   \r\n   contract caller{\r\n      address public constant calledaddress = 0x9d83e140330758a8fFD07F8Bd73e86ebcA8a5692;\r\n      function callering() public {\r\n           (bool success, bytes memory result) =calledaddress.call(abi.encodeWithSignature(\"count()\"));\r\n       \r\n           if(!success){\r\n               revert(\"error\");\r\n       }\r\n      }\r\n       function caller_error() public {\r\n           (bool success,bytes memory result) = calledaddress.call(abi.encodeWithSignature(\"count\")); //call一个不存在的函数\r\n           if(!success){\r\n               revert(\"error\");\r\n           }\r\n       }\r\n   }\r\n   ```\r\n\r\n   注意\r\n\r\n   1. called合约的地址，在remix上先进行部署就可以看到\r\n\r\n   2. 所有的address地址都有call方法，相当于内置函数\r\n\r\n   3. 这里有个细节，地址变量前面使用了constant，这个是不算在存储槽里面的，这在delegatecall方法里面非常重要，后续会讲。\r\n\r\n   4. 下面是一个处理失败的结果图片\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/NHKOyZfq66a4b9b6dbc6f.png)\r\n   5. 这里所在的上下文是在called 合约中，caller合约相当于是起到一个代理去调用的作用，还有另一种情况是将调用函数导入当前的上下文，相当于 import，但是有一点不一样，因为牵扯到存储槽的问题，你在caller中，存储槽是0，而导入进delegatecall中，存储槽0是caller的一个地址，调用了called合约，最终的结果是使得地址的值加一，造成了冲突。这个是delegatecall 与 call  的一个本质区别。\r\n\r\n## delegatecall方法\r\n\r\n### 特点\r\n\r\n1. 跟call一样，delegatecall也返回包含两个值的元组，bool success和   bytes  memory data\r\n\r\n2. 包括call方法，因为data都是经过了abi编码，但是并没有进行解码，下面是编码和解码的方式\r\n\r\n   编码：data =  abi.encode(value) [将  value类型的数据编码成bytes数据]\r\n\r\n   解码：data = abi.decode(data,(uint256))  解码data中的uint256数据   [从bytes类型的数据中，提取出uint256类型的值]\r\n\r\n### 存储\r\n\r\n可以知道，上下文不同是两个方法之间的本质区别。调用delegatefall方法尤其需要注意存储槽的问题。下面简单介绍，详细深入查看sodility文档。\r\n\r\n#### 存储原理\r\n\r\n1. 智能合约的存储三个特点：持久，可读，可写。一旦上链，合约将永久存在，合约的存储是开发是预先分配的，读取数据时免费的，但是写入数据需要gas，因为你改变了合约的状态。\r\n2. 智能合约只能读取自己上下文中的数据，如果要读取其他智能合约的数据，需要由目标合约定义接口。或者使用方法，但也需要看目标合约是否限制了访问权限。\r\n\r\n#### 存储槽\r\n\r\n1. 每个存储槽的空间大小是 32 字节\r\n\r\n2. 索引从0开始，跟数组一样。因为以太坊的存储系统使用 256 位宽的数据，意味着存储槽的索引可以是从 0 到 `2^256 - 1` 的任意值\r\n\r\n3. 存储槽就相当于一个数组，在这个数组中，每个对象对应一个状态变量。每个状态变量都会映射到一个槽(slot) 并且有基本类型\r\n\r\n4. 如果一个数据是少于32字节的，会连续多个数据打包在一个槽里面。\r\n\r\n   [uint256 是 256位，1字节等于8位， 相当于是32字节]，如果上一个空间有剩余，则会使用剩余空间的同时，再满足自身大小\r\n\r\n### 演示\r\n\r\n1. 合约代码大致一样，区别就是要在caller中声明num，因为是在当前的上下文中，上面的方法换成了 delegatecall 的方法，下面是存储槽冲突的一个结果。\r\n2. 变量名无所谓，只跟你的槽的索引有关系。最终是以你再caller中定义的状态变量为准，called只是执行逻辑。\r\n\r\n合约代码\r\n\r\n1. called\r\n\r\n   ```solidity\r\n   // SPDX-License-Identifier: MIT\r\n   pragma solidity ^0.8.0;\r\n   \r\n   contract called {\r\n       uint8 public num1 = 1;\r\n       function count() public returns (uint8) {\r\n           num++;\r\n           return num;\r\n       }\r\n   }\r\n   ```\r\n\r\n2. caller\r\n\r\n   ```solidity\r\n   // SPDX-License-Identifier: MIT\r\n   pragma solidity ^0.8.0;\r\n   \r\n   contract caller{\r\n      address public calledaddress = 0x9d83e140330758a8fFD07F8Bd73e86ebcA8a5692;\r\n      uint8 public num2;\r\n      function callering() public {\r\n           (bool success, bytes memory result) =calledaddress.delegatecall(abi.encodeWithSignature(\"count()\"));\r\n       \r\n           if(!success){\r\n               revert(\"error\");\r\n       }\r\n      }\r\n       function caller_error() public {\r\n           (bool success,bytes memory result) = calledaddress.call(abi.encodeWithSignature(\"count\")); //call一个不存在的函数\r\n           if(!success){\r\n               revert(\"error\");\r\n           }\r\n       }\r\n   }\r\n   ```\r\n\r\n   \r\n\r\n运行代码前\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/daii10LR66a4b9e07634c.png)\r\n运行代码后\r\n\r\n\r\n\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/pMcqsMLe66a4ba09cedf9.png)\r\n\r\n### 小结\r\n\r\n1. 我们可以先声明  num  之后再声明地址，这样存储槽就不冲突了，或者使用不可变和常量变量。\r\n2. delegatecall方法，上下文环境始终是在调用合约中，目标合约只执行逻辑，如果目标合约的状态变量a在存储槽0中，b在调用合约的存储槽0中，那么不管a的值为多少，始终等于b的值。当然存在特殊情况，不可变和常量变量不遵循。下面会讲述。\r\n\r\n### 优点\r\n\r\n这个方法实现了数据解耦，将对数据的处理和数据的使用分开，使得数据的产生与消费之间不直接依赖，如果要更改执行逻辑，那么只需要替换called合约即可，创建一个可以不断优化升级的合约。\r\n\r\n## 特殊场景\r\n\r\n本人也是看了一篇[很好的文章](https://learnblockchain.cn/article/8827)，关于这一块，引用了这篇文章的代码来解释。后续一些内容也参照了这篇文章，后续就当作我个人学习的理解吧\r\n\r\n### 不可变和常量变量\r\n\r\n```solidity\r\ncontract Caller {  \r\n    uint256 private immutable a = 3;  \r\n    function getValueDelegate(address called) public pure returns (uint256) {  \r\n        (bool success, bytes memory data) = B.delegatecall(  \r\n            abi.encodewithSignature(\"getValue()\"));  \r\n        return abi.decode(data, (uint256)); // is this 3 or 2?  \r\n    }  \r\n}  \r\n  \r\ncontract Called {  \r\n    uint256 private immutable a = 2;  \r\n  \r\n    function getValue() public pure returns (uint256) {  \r\n        return a;  \r\n    }  \r\n}\r\n```\r\n\r\n依据我之前的理解，这里返回的值应该是 3  ，但是实际的结果是 2。我也不理解，但是看了文章之后明白了。\r\n\r\n**不可变(immutable)或常量变量(constant)不是真正的状态变量：它们不占用槽** ,而是通过硬编码的方式，直接嵌入到合约的字节码里面，不依赖存储。当你调用了合约，实际上就是直接调用了这个变量，而不是通过槽映射到对应的值。\r\n\r\n### 全局变量\r\n\r\n1. msg.sender: 调用当前合约的账户地址\r\n\r\n2. msg.value：当前调用中附带的以太币数量\r\n\r\n3. msg,data : 调用中携带的原始数据 (输入数据)\r\n\r\n如果使用了delegatecall方法，在called合约中使用了msg.sender之类的全局变量，其msg.sender对应的是caller合约的msg.sender，因为其上下文始终是caller合约，而msg.sender的根据就是根据上下文而定的。\r\n\r\n### 委托调用\r\n\r\n#### delegatecall\r\n\r\n存在一种情况，我们使用第一个合约对第二个合约 发出delegatecall，第二个合约对第三个合约发出delegatecall，上下文保持-------第一个合约。最简单的理解，因为delegatecall方法，其核心就是使用当前调用合约的上下文，仅引用了被调用合约的代码执行逻辑。第二个的合约的所有代码逻辑被第一个合约引用，其所在的上下文就是第一个合约，那么第二个合约的代码也只是引用了第三个合约的代码逻辑，上下文是第二合约，而第二合约所在上下文就是第一个合约。如此一来，不管你委托了多少合约，都只以第一为准。那么提到的msg.sender也是一样，其始终指向的是第一个合约的调用者的地址。\r\n\r\n#### call\r\n\r\n如果我们的第二个合约使用了 call方法呢？也很简单，直接将第一和第二合约看成一个整体，毕竟第一合约只是使用了第二合约的代码逻辑，那么就变成了第一合约 call 第三合约，当第二和第三合约都使用了 msg.sender  ，那么第二合约的msg.sender指的是第一合约；第三合约的msg.sender就直接指第三合约，因为call 是使用目标合约的上下文。"},"author":{"user":"https://learnblockchain.cn/people/19204","address":"0x0c3743ac31156269ea0ea04bdb1864645017a92b"},"history":"bafkreidd3yq67i6tdlhfm2tfsfaehw5r3p7vuz6byyx3736a5ozjo26wdq","timestamp":1722087308,"version":1}