solidity:delegetecall 底层解释

223 阅读6分钟

前置知识(访问成员变量数据的底层机制--->存储位置寻找变量)

在以太坊智能合约的底层实现中,访问成员变量(状态变量)实际上是通过它们在合约存储中的位置来进行的。在Solidity中,尽管开发者是通过变量名来读写这些变量,但编译器在编译合约时会将这些变量名转换成对应的存储位置。让我们来深入了解一下这个过程:

存储布局

关于storage layout的详细解释在专栏之后的文章中 在智能合约中,状态变量被存储在一个称为“存储(Storage)”的特殊区域,这个区域被组织成一个巨大的键值存储数组。每个状态变量都分配了一个或多个存储槽,它们在这个数组中有特定的索引位置。

变量地址定位

  • 连续存储:Solidity将合约中声明的第一个状态变量放在存储的位置0,第二个变量放在位置1,以此类推。对于更复杂的数据类型(如数组和结构体),情况会变得更复杂,但原则是类似的。
  • 变量的存储位置计算:Solidity编译器负责确定每个变量的存储位置。这个位置取决于变量在合约中的声明顺序以及其他变量的数据类型和大小。

访问和修改

  • 当执行合约代码以读取或修改状态变量时,EVM根据这些变量的存储位置来执行相应的操作。
  • 即使在高级语言中使用变量名进行操作,最终这些操作都会被编译成EVM指令,这些指令使用存储位置来访问特定的状态变量。

底层操作

  • 在底层,没有直接使用变量名的概念。相反,EVM操作和Solidity生成的低级指令是基于存储位置的。
  • 对状态变量的读写操作转换为对应的EVM指令,这些指令指向变量的存储位置。

总结

在Solidity和以太坊虚拟机的底层,状态变量的访问和修改完全依赖于它们在合约存储中的位置,而不是变量名。变量名在源代码级别为开发者提供了便利,但在编译过程中,这些名字会被转换为对应的存储位置。这种设计使得智能合约的存储操作既高效又一致。

重要性

  • 优化:了解存储布局可以帮助优化Gas消耗,特别是对于数组和结构体的存储和访问。
  • 安全性:在使用delegatecall等低级功能时,了解存储布局对于避免安全漏洞至关重要。
  • 升级:对于可升级合约,保持兼容的存储布局是关键。

总之,Solidity合约的存储布局是由其状态变量的类型和声明顺序决定的。了解这一布局对于编写高效和安全的智能合约是非常重要的。

底层访问成员变量、存储布局、delegatecall的关系

在Solidity中,深入理解底层访问成员变量、存储布局以及delegatecall的关系是至关重要的,尤其是在处理高级合约逻辑和合约升级时。让我们逐一解析这些概念。

底层访问成员变量

在智能合约中,成员变量(状态变量)存储在以太坊虚拟机(EVM)的特定存储区域,每个变量被分配一个或多个“存储槽”(storage slot)。在底层,这些变量不是通过名称访问的,而是通过它们在存储中的位置访问的。

存储布局

存储布局是指状态变量在合约存储中的排列方式。Solidity遵循特定的规则来布局这些变量:

  1. 顺序:状态变量按它们在合约中声明的顺序排列。
  2. 固定大小类型:每个简单固定大小类型的状态变量(如uint256address)通常占据一个存储槽。
  3. 动态大小类型:动态大小的类型(如数组和映射)在它们自己的存储槽中存储一个指针,指向实际数据的起始位置。

delegatecall的关系

delegatecall是一个低级函数,允许一个合约(A)执行另一个合约(B)的代码,但在合约A的上下文(包括存储)中。这意味着虽然执行的是合约B的代码,但所有的状态变更都会影响合约A。

delegatecall:委托调用,即委托另一个合约在本合约的环境下执行操作。

delegatecall和存储布局的重要性

当使用delegatecall时,理解存储布局变得极其重要。因为:

  1. 状态变量覆盖:如果合约B在其存储布局中有一个状态变量在同一位置上,该变量的改变会覆盖合约A中相同位置的状态变量。
  2. 潜在风险:如果合约A和B的存储布局不兼容,通过delegatecall调用B的函数可能导致不可预测的行为和严重的安全漏洞。

例子

假设有两个合约A和B,它们各有一个状态变量:

solidityCopy code
contract A {
    uint public valueA;
    // ...其他函数和逻辑...
}

contract B {
    uint public valueB;
    function updateValue() public {
        valueB = 5;
    }
}

如果合约A使用delegatecall来调用合约B的updateValue函数,该调用将更新合约A中的valueA变量,而不是合约B的valueB,因为这两个变量在各自合约的存储中占据相同的位置。

结论

在使用delegatecall时,开发者必须确保合约之间的存储布局是兼容的。在设计可升级合约或库合约时,这一点尤其重要。合约的存储布局应该被认为是合约接口的一部分,需要谨慎修改以防止意外的行为或漏洞。

新的补充知识

delegatecall 会被 call截断。

  1. delegatecall调用另一合约,这个合约不能嵌入调用自己的call,也不能嵌入调用自己的delegatecall,也不能通过this调用(相当于用call),功能设计上也没有这样的必要场景,solidity禁止这样做是合理的。这一点要非常关注!
  2. delegatecall中,合约之间的delegatecall,call都没有问题。当delegatecall中出现合约之间的call时,delegate语义被切断,被call的合约执行call语义,不受之前的delegatecall影响
  3. 一个合约只有被非delegatecall调用,才会成为上下文主体,否则它“从属于”那个通过delegatecall调用它的合约 waring
  4. 合约内部函数之间的调用,应避免产生新的上下文;
  5. 如果external关键字修饰的函数需要被内部调用,应将其变为public,避免使用this关键字来调用。