Solidity之DelegateCall委托调用详解

前言

在编写以太坊智能合约代码时,有些情况下(功能的解耦,分区明确)我们需要与其他合约进行交互。如果只是为了代码复用,可把公共代码单独抽离出来,部署到一个library中(类似util工具包),后面就可以直接引入使用了。但是library中不允许定义任何storage类型的变量,这就意味着library不能修改合约的状态。如果需要修改合约状态变量,我们就需要另外部署一个新合约,具体怎么操作呢?

贴个官方文档说明:

There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.

 通俗地理解就是如何调用另一个合约函数。

三种调用函数

在 Solidity 中,call 函数簇可以实现跨合约的函数调用功能,其中包括 call、delegatecall 和 callcode 三种方式。

  • 调用模式

<address>.call(...) returns (bool)

<address>.callcode(...) returns (bool)

<address>.delegatecall(...) returns (bool)

这些函数提供了灵活的方式与合约进行交互,并且可以接受任何长度、任何类型的参数,其传入的参数会被填充至 32 字节最后拼接为一个字符串序列,由 EVM 解析执行。在函数调用的过程中,Solidity 中的内置变量 msg 会随着调用的发起而改变,msg 保存了调用方的信息包括:调用发起的地址,交易金额,被调用函数字符序列等。

异同点

  • call: 调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境
  • delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境(相当于复制被调用者的代码到调用者合约)
  • callcode: 调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境

警告:"callcode"已被弃用,取而代之的是" delegatcall "。

delegatecall 与 call 的区别

1、如果我们知道目标合约ABI,可以直接使用函数签名调用。

假设我们已经部署了一个名为“Storage”的简单合约,它允许用户保存一个val状态变量值。

pragma solidity ^0.5.8;
contract Storage {
    uint public val;
constructor(uint v) public {
        val = v;
    }
function setValue(uint v) public {
        val = v;
    }
}

我们要部署另一个名为“Machine”的合约,它是“Storage”合约的调用者。“Machine”引用“Storage”合约并更改val值,如下。

pragma solidity ^0.5.8;
import "./Storage.sol";
contract Machine {
    Storage public s;
constructor(Storage addr) public {
        s = addr;
        calculateResult = 0;
    }
    
    function saveValue(uint x) public returns (bool) {
        s.setValue(x);
        return true;
    }
function getValue() public view returns (uint) {
        return s.val();
    }
}

在这种情况下,我们知道“Storage”的 ABI 及其地址,这样我们就可以用地址初始化现有的“Storage”合约,ABI 告诉我们如何调用“Storage”合约的函数。 我们可以看到“Machine”合约调用“Storage”setValue()函数。

2、如果我们不知道目标合约 ABI,请使用 call 或 delegatecall调用。

在解释call()和delegatcall()之前,了解EVM如何保存合约变量以理解后续内容将有很有帮助。

EVM如何保存字段变量到存储

在以太坊中,有两种保存合约字段变量的空间。一个是“memory”,另一个是“storage”。' foo is saved to storage' 意味着' foo'的值被永久记录到区块链上了。
那么,一个合约中如此多的变量怎么做到不重叠彼此的地址空间呢?

EVM为字段变量分配槽号(slot number),有点抽象哈。

contract Sample1 {
    uint256 first;  // slot 0
    uint256 second; // slot 1
}

 因为first在“Sample1”中首先声明,所以它被分配了0号槽位。每个不同的变量由它的槽号来区分。

在EVM中,它在智能合约存储中有2^256个插槽,每个插槽可以保存32字节大小的数据。 

 在我们讨论如何调用智能合约函数时,有一个词Context。实际上,语境在软件中是一个比较普遍的概念,它的意义会随着语境的变化而变化。当我们讨论程序的执行时,我们可以说上下文是所有环境,比如执行点的变量或状态。例如,在程序A的执行点上,执行该程序的用户名是zeroFruit,那么用户名zeroFruit可以是程序A的上下文。在以太坊智能合约中,有很多Context,其中一个代表性的事情是“who execute this contract”。你可能会被大量的solidity代码中看到msg.sender和msg.value。发送方地址因执行该合约函数的人而异。

顾名思义,DelegateCall是调用方合约如何调用目标合约函数的调用机制,但当目标合约执行其逻辑时,上下文不是在执行调用方合约的用户上,而是在调用方合约上。

 那么当合约委托调用到目标合约时,存储的状态将如何改变?
上下文是在调用方合约上的,所有的状态变化逻辑反映在调用方的存储上。
例如,我们有代理合约和业务合约。代理合约将调用委托给业务合约功能。如果用户调用代理合约,代理合约将把调用委托给业务合约,并执行对应功能。而所有的状态变化都将反映在代理合约存储中,而不是业务合约中。

delegatecall合约测试

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

// NOTE: Deploy this contract first
contract OLD {
    // NOTE: storage layout must be the same as contract A
    uint public num;
    address public sender;
    uint public value;

    function setVars(uint _num) public payable {
        num = _num;
        sender = msg.sender;
        value = msg.value;
    }
}

contract New1 {
    uint public num;
    address public sender;
    uint public value;

    function setVars(address _contract, uint _num) public payable {
        // A's storage is set, B is not modified.
        (bool success, bytes memory data) = _contract.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}

contract New2 {
    uint public num;
    address public sender;
    uint public value;

    function setVars(address _contract, uint _num) public payable {
        // A's storage is set, B is not modified.
        (bool success, bytes memory data) = _contract.call(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}

contract New3 {
    uint public num1;
    uint public value1;
    uint public sender1;

    function setVars(address _contract, uint _num) public payable {
        // A's storage is set, B is not modified.
        (bool success, bytes memory data) = _contract.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}

 关于如何操作此合约的步骤,我会单独录制一小段视频来介绍,有兴趣或动手能力强的小伙伴可以对照着注释调试哈。其实呢delegatecall函数设计的初衷是使用给定地址的代码,其他信息则使用当前合约(如存储、余额等),某种程度上也是为了代码的复用吧。

欢迎━(*`∀´*)ノ亻!博友们发现问题和提出宝贵建议,谢谢!

参考资料:https://medium.com/coinmonks/delegatecall-calling-another-contract-function-in-solidity-b579f804178c

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Solidity 中,函数访问修饰符用于限制函数的访问权限,以确保只有满足特定条件的地址或账户才能调用该函数。以下是 Solidity 中常用的访问修饰符及其用法: 1. public:公共函数,任何地址都可以调用。 2. private:私有函数,只能在当前合约内部调用。 3. internal:内部函数,只能在当前合约内部或继承该合约的合约中访问。 4. external:外部函数,只能通过外部交互(如消息调用)方式调用,不能在合约内部直接调用。 以下是一个简单示例,演示如何使用访问修饰符限制函数的访问权限: ``` pragma solidity ^0.8.0; contract AccessControl { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this function"); _; } function changeOwner(address _newOwner) public onlyOwner { owner = _newOwner; } function publicFunction() public { // 可以被任何地址调用 } function privateFunction() private { // 只能在当前合约内部调用 } function internalFunction() internal { // 只能在当前合约内部或继承该合约的合约中访问。 } function externalFunction() external { // 只能通过外部交互(如消息调用)方式调用,不能在合约内部直接调用。 } } ``` 在上述示例中,我们定义了一个 onlyOwner 修饰符,用于限制只有合约所有者才能调用 changeOwner 函数。而 publicFunction 可以被任何地址调用,而 privateFunction 只能在当前合约内部调用,internalFunction 只能在当前合约内部或继承该合约的合约中访问,externalFunction 只能通过外部交互方式调用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值