生动理解call方法与delegatecall方法

可以清晰地了解,当作入门认识,因为深入的话会牵扯到很多底层的东西,这里提到的一点点这作为了解这两个方法的辅助

在了解两个方法前,需要提到一个概念: ABI

什么是ABI

ABI :根据英文 “Application Binary Interface”翻译过来就是,应用程序二进制接口

ABI的作用

应用程序可以与智能合约进行交互,比如发送交易、查询状态、获取事件。在链上,我们所有的数据都是以字节码的形式存在,所以当我们调用某一个合约,相当于对该合约发送一个交易,其中交易的内容,传输的数据,指令,就是预先进行了abi编码进行传输的,之后匹配合约的字节码,进行调用。

ABI编码

函数选择器:对函数签名,去前面四个字节,使用 Keccak-256加密哈希函数进行计算得到结果

call方法

特点

  1. 返回一个元组,包含两个值,一个是布尔值,表示交易成功或失败,另一个是字节(bytes),其字节是执行函数的返回值经过了abi编码(如果执行函数无返回值则返回为空)

  2. 没有gas限制,可以支持对方合约fallback()receive()函数实现复杂逻辑

  3. 转账失败,不会revert,所以通常需要与 revert 函数一同使用,revert会撤销自事务开始以来对区块链状态的所有更改

演示

我们来看看实例,就以数字增加的简单代码来演示

  1. 被 called 的代码

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract called {
       uint8 public num = 1;
       function count() public returns (uint8) {
           num++;
           return num;
       }
    }
  2. caller的代码

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract caller{
      address public constant calledaddress = 0x9d83e140330758a8fFD07F8Bd73e86ebcA8a5692;
      function callering() public {
           (bool success, bytes memory result) =calledaddress.call(abi.encodeWithSignature("count()"));
    
           if(!success){
               revert("error");
       }
      }
       function caller_error() public {
           (bool success,bytes memory result) = calledaddress.call(abi.encodeWithSignature("count")); //call一个不存在的函数
           if(!success){
               revert("error");
           }
       }
    }

    注意

    1. called合约的地址,在remix上先进行部署就可以看到

    2. 所有的address地址都有call方法,相当于内置函数

    3. 这里有个细节,地址变量前面使用了constant,这个是不算在存储槽里面的,这在delegatecall方法里面非常重要,后续会讲。

    4. 下面是一个处理失败的结果图片 image.png

    5. 这里所在的上下文是在called 合约中,caller合约相当于是起到一个代理去调用的作用,还有另一种情况是将调用函数导入当前的上下文,相当于 import,但是有一点不一样,因为牵扯到存储槽的问题,你在caller中,存储槽是0,而导入进delegatecall中,存储槽0是caller的一个地址,调用了called合约,最终的结果是使得地址的值加一,造成了冲突。这个是delegatecall 与 call 的一个本质区别。

delegatecall方法

特点

  1. 跟call一样,delegatecall也返回包含两个值的元组,bool success和 bytes memory data

  2. 包括call方法,因为data都是经过了abi编码,但是并没有进行解码,下面是编码和解码的方式

    编码:data = abi.encode(value) [将 value类型的数据编码成bytes数据]

    解码:data = abi.decode(data,(uint256)) 解码data中的uint256数据 [从bytes类型的数据中,提取出uint256类型的值]

存储

可以知道,上下文不同是两个方法之间的本质区别。调用delegatefall方法尤其需要注意存储槽的问题。下面简单介绍,详细深入查看sodility文档。

存储原理

  1. 智能合约的存储三个特点:持久,可读,可写。一旦上链,合约将永久存在,合约的存储是开发是预先分配的,读取数据时免费的,但是写入数据需要gas,因为你改变了合约的状态。
  2. 智能合约只能读取自己上下文中的数据,如果要读取其他智能合约的数据,需要由目标合约定义接口。或者使用方法,但也需要看目标合约是否限制了访问权限。

存储槽

  1. 每个存储槽的空间大小是 32 字节

  2. 索引从0开始,跟数组一样。因为以太坊的存储系统使用 256 位宽的数据,意味着存储槽的索引可以是从 0 到 2^256 - 1 的任意值

  3. 存储槽就相当于一个数组,在这个数组中,每个对象对应一个状态变量。每个状态变量都会映射到一个槽(slot) 并且有基本类型

  4. 如果一个数据是少于32字节的,会连续多个数据打包在一个槽里面。

    [uint256 是 256位,1字节等于8位, 相当于是32字节],如果上一个空间有剩余,则会使用剩余空间的同时,再满足自身大小

演示

  1. 合约代码大致一样,区别就是要在caller中声明num,因为是在当前的上下文中,上面的方法换成了 delegatecall 的方法,下面是存储槽冲突的一个结果。
  2. 变量名无所谓,只跟你的槽的索引有关系。最终是以你再caller中定义的状态变量为准,called只是执行逻辑。

合约代码

  1. called

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract called {
       uint8 public num1 = 1;
       function count() public returns (uint8) {
           num++;
           return num;
       }
    }
  2. caller

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract caller{
      address public calledaddress = 0x9d83e140330758a8fFD07F8Bd73e86ebcA8a5692;
      uint8 public num2;
      function callering() public {
           (bool success, bytes memory result) =calledaddress.delegatecall(abi.encodeWithSignature("count()"));
    
           if(!success){
               revert("error");
       }
      }
       function caller_error() public {
           (bool success,bytes memory result) = calledaddress.call(abi.encodeWithSignature("count")); //call一个不存在的函数
           if(!success){
               revert("error");
           }
       }
    }

运行代码前

image.png 运行代码后

image.png

小结

  1. 我们可以先声明 num 之后再声明地址,这样存储槽就不冲突了,或者使用不可变和常量变量。
  2. delegatecall方法,上下文环境始终是在调用合约中,目标合约只执行逻辑,如果目标合约的状态变量a在存储槽0中,b在调用合约的存储槽0中,那么不管a的值为多少,始终等于b的值。当然存在特殊情况,不可变和常量变量不遵循。下面会讲述。

优点

这个方法实现了数据解耦,将对数据的处理和数据的使用分开,使得数据的产生与消费之间不直接依赖,如果要更改执行逻辑,那么只需要替换called合约即可,创建一个可以不断优化升级的合约。

特殊场景

本人也是看了一篇很好的文章,关于这一块,引用了这篇文章的代码来解释。后续一些内容也参照了这篇文章,后续就当作我个人学习的理解吧

不可变和常量变量

contract Caller {  
    uint256 private immutable a = 3;  
    function getValueDelegate(address called) public pure returns (uint256) {  
        (bool success, bytes memory data) = B.delegatecall(  
            abi.encodewithSignature("getValue()"));  
        return abi.decode(data, (uint256)); // is this 3 or 2?  
    }  
}  

contract Called {  
    uint256 private immutable a = 2;  

    function getValue() public pure returns (uint256) {  
        return a;  
    }  
}

依据我之前的理解,这里返回的值应该是 3 ,但是实际的结果是 2。我也不理解,但是看了文章之后明白了。

不可变(immutable)或常量变量(constant)不是真正的状态变量:它们不占用槽 ,而是通过硬编码的方式,直接嵌入到合约的字节码里面,不依赖存储。当你调用了合约,实际上就是直接调用了这个变量,而不是通过槽映射到对应的值。

全局变量

  1. msg.sender: 调用当前合约的账户地址

  2. msg.value:当前调用中附带的以太币数量

  3. msg,data : 调用中携带的原始数据 (输入数据)

如果使用了delegatecall方法,在called合约中使用了msg.sender之类的全局变量,其msg.sender对应的是caller合约的msg.sender,因为其上下文始终是caller合约,而msg.sender的根据就是根据上下文而定的。

委托调用

delegatecall

存在一种情况,我们使用第一个合约对第二个合约 发出delegatecall,第二个合约对第三个合约发出delegatecall,上下文保持-------第一个合约。最简单的理解,因为delegatecall方法,其核心就是使用当前调用合约的上下文,仅引用了被调用合约的代码执行逻辑。第二个的合约的所有代码逻辑被第一个合约引用,其所在的上下文就是第一个合约,那么第二个合约的代码也只是引用了第三个合约的代码逻辑,上下文是第二合约,而第二合约所在上下文就是第一个合约。如此一来,不管你委托了多少合约,都只以第一为准。那么提到的msg.sender也是一样,其始终指向的是第一个合约的调用者的地址。

call

如果我们的第二个合约使用了 call方法呢?也很简单,直接将第一和第二合约看成一个整体,毕竟第一合约只是使用了第二合约的代码逻辑,那么就变成了第一合约 call 第三合约,当第二和第三合约都使用了 msg.sender ,那么第二合约的msg.sender指的是第一合约;第三合约的msg.sender就直接指第三合约,因为call 是使用目标合约的上下文。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
浪迹陨灭
浪迹陨灭
0x0c37...a92b
专注于solidity智能合约的开发