15. Solidity:call和delegate call

15.1 call

call 是address類型的低級成員函數(shù),它用來與其他合約交互。它的返回值為(bool, data),分別對應(yīng)call是否成功以及目標(biāo)函數(shù)的返回值。

  • call是address類型的低級成員函數(shù),因此可以直接使用合約地址對合約進行調(diào)用,無需使用合約類型。
  • call是solidity官方推薦的通過觸發(fā)fallback或receive函數(shù)發(fā)送ETH的方法。
  • 不推薦用call來調(diào)用另一個合約,因為當(dāng)你調(diào)用不安全合約的函數(shù)時,你就把主動權(quán)交給了它。推薦的方法仍是聲明合約變量后調(diào)用函數(shù),見14. Solidity:調(diào)用其他合約。
  • 當(dāng)我們不知道對方合約的源代碼或ABI,就沒法生成合約變量;這時,我們?nèi)钥梢酝ㄟ^call調(diào)用對方合約的函數(shù)。

15.1.1 call的使用規(guī)則

call的使用規(guī)則如下:

目標(biāo)合約地址.call(二進制編碼);

其中二進制編碼利用結(jié)構(gòu)化編碼函數(shù)abi.encodeWithSignature獲得:

abi.encodeWithSignature("函數(shù)簽名", 逗號分隔的具體參數(shù))

函數(shù)簽名為"函數(shù)名(逗號分隔的參數(shù)類型)"。例如:

abi.encodeWithSignature("foo(uint256,address)", _x, _addr)

注意:uint類型必須寫成uint256。

另外call在調(diào)用合約時可以指定交易發(fā)送的ETH數(shù)額和gas:

目標(biāo)合約地址.call{value:發(fā)送數(shù)額, gas:gas數(shù)額}(二進制編碼);

15.1.2 call的使用示例

首先,先編寫一個測試合約,用于其他合約使用call方式對其進行調(diào)用:

contract TestContract{
    string public message;
    uint public x;
    event Log(string message);

    function foo(string memory _message, uint _x) external payable returns (uint) {
        message = _message;
        x = _x;
        return (uint(0x12345));
    }


    fallback() external {
        emit Log("fallback was called");
     }

    function getBalance() external view returns (uint) {
        return address(this).balance;
    }
}

測試合約中有三個函數(shù):

  • foo():設(shè)置兩個狀態(tài)變量,可接收ETH;
  • fallback():fallback()函數(shù),調(diào)用不存在的函數(shù)時會調(diào)用;
  • getBalance():助手函數(shù),獲取合約余額;

編寫一個合約,使用call方式調(diào)用測試合約:

contract CallTestContract {
    event Log(bool success, bytes data);
    function callFoo(address _t, uint _gas) external payable {
        (bool success, bytes memory data) = _t.call{value:msg.value, gas:_gas}(abi.encodeWithSignature("foo(string,uint256)", "call foo", 1234567));
        emit Log(success, data);
    }

    function callFuncNotExist(address _t)external {
        (bool success, ) = _t.call(abi.encodeWithSignature("foo(string,uint)"));
        emit Log(success, "");
    }
}
  • callFoo():調(diào)用測試合約的foo()函數(shù),將所有收到的ETH都轉(zhuǎn)入測試合約中,并且可以自定義gas。
  • callFuncNotExist():調(diào)用測試合約中不存在的函數(shù):會調(diào)用測試合約的fallback()函數(shù)。

15.1.3 call的測試結(jié)果

傳入111 wei以太坊代幣,gas設(shè)置為5000,調(diào)用callFoo()結(jié)果:

gas費不足,調(diào)用失敗

// gas費不足,調(diào)用失敗
[
    {
        "from": "0x0498B7c793D7432Cd9dB27fb02fc9cfdBAfA1Fd3",
        "topic": "0x138eba290365ed63784a3b85ff5ccb9a818dc0cc4fbab4fccaa244345f0c6c38",
        "event": "Log",
        "args": {
            "0": false,
            "1": "0x",
            "success": false,
            "data": "0x"
        }
    }
]

傳入111 wei以太坊代幣,gas設(shè)置為500000,調(diào)用callFoo()結(jié)果:

調(diào)用成功,并且收到了返回值:0x0000000000000000000000000000000000000000000000000000000000012345

// 調(diào)用成功,并且收到了返回值:0x0000000000000000000000000000000000000000000000000000000000012345
[
    {
        "from": "0x0498B7c793D7432Cd9dB27fb02fc9cfdBAfA1Fd3",
        "topic": "0x138eba290365ed63784a3b85ff5ccb9a818dc0cc4fbab4fccaa244345f0c6c38",
        "event": "Log",
        "args": {
            "0": true,
            "1": "0x0000000000000000000000000000000000000000000000000000000000012345",
            "success": true,
            "data": "0x0000000000000000000000000000000000000000000000000000000000012345"
        }
    }
]

調(diào)用callFuncNotExist()方法:

調(diào)用成功,返回值為空。
回退函數(shù)的event被觸發(fā),說明調(diào)用了回退函數(shù)。

// 調(diào)用成功,返回值為空。
// 回退函數(shù)的event被觸發(fā),說明調(diào)用了回退函數(shù)。
[
    {
        "from": "0xEf9f1ACE83dfbB8f559Da621f4aEA72C6EB10eBf",
        "topic": "0xcf34ef537ac33ee1ac626ca1587a0a7e8e51561e5514f8cb36afa1c5102b3bab",
        "event": "Log",
        "args": {
            "0": "fallback was called",
            "message": "fallback was called"
        }
    },
    {
        "from": "0x0498B7c793D7432Cd9dB27fb02fc9cfdBAfA1Fd3",
        "topic": "0x138eba290365ed63784a3b85ff5ccb9a818dc0cc4fbab4fccaa244345f0c6c38",
        "event": "Log",
        "args": {
            "0": true,
            "1": "0x",
            "success": true,
            "data": "0x"
        }
    }
]

將測試合約的回退函數(shù)注釋,重新部署,再次調(diào)用callFuncNotExist()方法:

調(diào)用了測試合約中不存在的方法,并且不存在回退函數(shù),因此call調(diào)用失敗。

// 調(diào)用了測試合約中不存在的方法,并且不存在回退函數(shù),因此call調(diào)用失敗。
[
    {
        "from": "0xB57ee0797C3fc0205714a577c02F7205bB89dF30",
        "topic": "0x138eba290365ed63784a3b85ff5ccb9a818dc0cc4fbab4fccaa244345f0c6c38",
        "event": "Log",
        "args": {
            "0": false,
            "1": "0x",
            "success": false,
            "data": "0x"
        }
    }
]

15.2 delegatecall

delegatecall與call類似,是solidity中地址類型的低級成員函數(shù)。與call不同的是,delegatecall并不改變被調(diào)用合約的狀態(tài),例如:

A調(diào)用B,發(fā)送100 wei;
B調(diào)用C,發(fā)送50 wei。
A —— B —— C

  • 如果B calls C,那么:

    1. C合約中:msg.sender = B
    2. C合約中:msg.value = 50
    3. C合約中狀態(tài)變量改變,50 wei留在C合約中
  • 如果B delegatecalls C,那么:

    1. C合約中:msg.sender = A
    2. C合約中:msg.value = 100
    3. B合約中狀態(tài)變量改變,100 wei留在B合約中,C合約狀態(tài)變量不會改變

15.2.1 delegatecall舉例

首先實現(xiàn)一個測試合約,用來被其他合約調(diào)用:

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

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

delegatecall測試合約:

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

    // call
    event Log(bool success, bytes data);
    function callSetVars(address _t, uint _num) external payable {
        // encodeWithSelector call
        (bool success, bytes memory data) = _t.call{value:50}(abi.encodeWithSelector(C.setVars.selector, _num));
        emit Log(success, data);
    }

    // delegatecall
    function delegatecallSetVars(address _t, uint _num) external payable {
        (bool success, bytes memory data) = _t.delegatecall(abi.encodeWithSignature("setVars(uint256)", _num));
        emit Log(success, data);
    }
}

測試結(jié)果

  • callSetVars函數(shù):錢包A調(diào)用,攜帶111 wei,參數(shù)為C合約地址,num=777。調(diào)用結(jié)果:
    1. C合約狀態(tài)變量被改變:num=777,sender=B合約地址,value=50;
    2. B合約狀態(tài)變量不變。
  • delegatecallSetVars函數(shù):錢包A調(diào)用,攜帶111 wei,參數(shù)為C合約地址,num=777。調(diào)用結(jié)果:
    1. B合約狀態(tài)變量被改變:num=777,sender=A錢包地址,value=111;
    2. C合約狀態(tài)變量不變。

15.2.2 delegatecall總結(jié)

當(dāng)用戶A通過合約B來call合約C的時候,執(zhí)行的是合約C的函數(shù),語境(Context,可以理解為包含變量和狀態(tài)的環(huán)境)也是合約C的:msg.sender是B的地址,并且如果函數(shù)改變一些狀態(tài)變量,產(chǎn)生的效果會作用于合約C的變量上。


call

當(dāng)用戶A通過合約B來delegatecall合約C的時候,執(zhí)行的是合約C的函數(shù),但是語境仍是合約B的:msg.sender是A的地址,并且如果函數(shù)改變一些狀態(tài)變量,產(chǎn)生的效果會作用于合約B的變量上。


delegatecall

注意:

  1. 和call不一樣,delegatecall在調(diào)用合約時可以指定交易發(fā)送的gas,但不能指定發(fā)送的ETH數(shù)額(value)。

  2. delegatecall有安全隱患,使用時要保證當(dāng)前合約和目標(biāo)合約的狀態(tài)變量存儲結(jié)構(gòu)相同,并且目標(biāo)合約安全,不然會造成資產(chǎn)損失。

15.2.3 delegatecall使用場景

  1. 代理合約(Proxy Contract):將智能合約的存儲合約和邏輯合約分開:代理合約(Proxy Contract)存儲所有相關(guān)的變量,并且保存邏輯合約的地址;所有函數(shù)存在邏輯合約(Logic Contract)里,通過delegatecall執(zhí)行。當(dāng)升級時,只需要將代理合約指向新的邏輯合約即可。

  2. EIP-2535 Diamonds(鉆石):鉆石是一個支持構(gòu)建可在生產(chǎn)中擴展的模塊化智能合約系統(tǒng)的標(biāo)準(zhǔn)。鉆石是具有多個實施合同的代理合同。 更多信息請查看:鉆石標(biāo)準(zhǔn)簡介

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容