sodility-call&delegatecall&代理合约

  1. sodility call和delegatecall
  2. call
  3. delegatecall
  4. 什么情况下会用到delegatecall?
  5. 合约升级

sodility call和delegatecall

call 和 delegatecall 都是 address 类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data),分别对应 call 是否成功以及目标函数的返回值。

call

先来说说call

  • call是solidity官方推荐的通过触发fallback或receive函数发送ETH的方法,_to.call{value: AMOUNT}("")后面的传参直接为空,就是发送代币,而不是调用合约方法
  • 不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数。
  • 当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数。

我们可以利用结构化编码函数abi.encodeWithSignature来帮我们生成编码code,然后再通过call来进行调用合约方法。具体的可以看solidity用abi调用智能合约的方法详解一文。

另外call在调用合约时可以指定交易发送的ETH数额和gas:
_address.call{value:发送数额, gas:gas数额}(二进制编码);

我们来看下具体怎么用,下面是一个简单的合约 call.sol ,foo函数用来接收参数x和msg.value,注意当需要接收外部的代币时,需要指定方法为payable。

// SPDX-License-Identifier: MIT

// call.sol
pragma solidity ^0.8.17;

contract Callled {
    uint256 public x;
    uint256 public balance;

    function foo(uint256 _x) external payable returns (string memory) {
        x = _x;
        balance = msg.value;
        return "foo called";
    }
}

再来看看调用合约,用法就是这么简单,Call方法里的address需要是call合约的地址,而如果需要通过call发送代币,则需要加上payable修饰符,且在call方法后面加个大括号,传入需要发送的代币量,此时call合约中的foo方法就可以直接拿到传递过去的msg.value了,同时我们还可以再大括号中指定用多少gas

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

contract CallFoo {
    function Call(address _address, uint256 _num) external payable {
        (bool success, ) = _address.call{value: msg.value, gas: 300000}(
            abi.encodeWithSignature("foo(uint256)", _num)
        );
        require(success, "call fail");
    }
}

部署测试
b568959a-34eb-4a1e-80eb-00ba2f730095-image.png

delegatecall

delegatecall与call类似,是solidity中地址类型的低级成员函数。delegate中是委托/代表的意思,那么delegatecall委托了什么?

当用户A通过合约B来call合约C的时候,执行的是合约C的函数,语境(Context,可以理解为包含变量和状态的环境)也是合约C的:msg.sender是B的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上。
93d7ab52-f732-48ce-8809-fe7e8677fa59-image.png

而当用户A通过合约B来delegatecall合约C的时候,执行的是合约C的函数,但是语境仍是合约B的:msg.sender是A的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。也就是说合约c只是处理逻辑,修改的状态结果最终会保存在B合约上。
adb12196-7334-423c-9e67-835a9c2f08f4-image.png

使用方式基本上和call一样,但有一点不一样,就这一点至关重要,delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额

注意:delegatecall有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。

什么情况下会用到delegatecall?

目前delegatecall主要有两个应用场景:

  • 代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。因为合约一旦部署上链,是没有办法再进行修改的,此时我们就可以通过代理合约,来进行合约升级,此时只需要将代理合约地址指向新的升级后的逻辑合约即可。

  • EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合约的代理合约。

合约升级

就代理合约,下面来举个例子看看:

假设我有一个代理合约delegateContrace.sol,一个逻辑合约A.sol,代码分别如下,有一点需要注意,代理合约和逻辑合约的变量类型以及顺序必须完全一致,并且虽然逻辑合约中修改的状态最终作用于代理合约,但是我们仍然需要声明变量类型,否则编译会报错。

  • delegateContrace.sol
    ```solidity
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;

    contract delegateContract {
    uint256 public x;
    string public y;
    address public sender;

    function setXY(
    address _address,
    uint256 _x,
    string memory _y
    ) external returns (bool) {
    sender = msg.sender;
    (bool success, ) = _address.delegatecall(
    abi.encodeWithSignature(“setXY(uint256,string)”, _x, _y)
    );
    return success;
    }
    }
    ```

  • A.sol
    ```solidity
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;

    contract A {
    uint256 public x;
    string public y;
    address public sender;

    function setXY(
    uint256 _x,
    string memory _y
    ) external payable returns (string memory) {
    sender = msg.sender;
    x = _x;
    y = _y;
    return “setXY called”;
    }
    }
    ```

先分别部署测试一下,我们可以看到,逻辑合约中变量都是初始变量,并没有改变,而代理合约中的变量已经被修改了。
79308089-1632-4923-a6cc-044035a9bd4e-image.png

当然,不通过代理合约,我们也可以直接调用逻辑合约A的方法,此时A的变量会改变成为新的值,但是代理合约中的变量并不会随之更改,如下图所示
784031f6-4739-40a4-b851-f5c36607731f-image.png

之前就说了,代理合约可以用来做合约升级,那如果我的处理逻辑需要改变,此时A合约就已经不适用了,那我们需要重新部署一个新合约B,来进行合约升级.

同样的,变量类型及顺序需要完全一致,我们修改了处理逻辑,把数字x加了1,而把字符串前面拼接了new string,下面是合约B的代码

  • B.sol
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    contract B {
        uint256 public x;
        string public y;
        address public sender;
    
        function setXY(
            uint256 _x,
            string memory _y
        ) external payable returns (string memory) {
            sender = msg.sender;
            x = _x + 1;
    
            bytes memory _ba = bytes(_y);
            bytes memory _bb = bytes("new string ");
            string memory ret = new string(_ba.length + _bb.length);
            bytes memory bret = bytes(ret);
            uint k = 0;
            for (uint i = 0; i < _bb.length; i++) bret[k++] = _bb[i];
            for (uint i = 0; i < _ba.length; i++) bret[k++] = _ba[i];
            y = string(ret);
    
            return "setXY called";
        }
    }
    

此时只需要我们把代理合约的地址指向新的逻辑合约B,即可实现合约的升级,此时我们可以看到结果,代理合约中的变量已经变成了 101new string hello,这说明我们的处理逻辑已经变成了新的,并且代理合约中的变量并没有重置,而是在原有的基础上进行的修改,这样我们就通过delegatecall代理合约是现实了合约升级。
0b256ea6-e0da-4bd1-af43-cc3b3731c069-image.png


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com