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,那么:
- C合約中:msg.sender = B
- C合約中:msg.value = 50
- C合約中狀態(tài)變量改變,50 wei留在C合約中
如果B delegatecalls C,那么:
- C合約中:msg.sender = A
- C合約中:msg.value = 100
- 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é)果:
- C合約狀態(tài)變量被改變:num=777,sender=B合約地址,value=50;
- B合約狀態(tài)變量不變。
- delegatecallSetVars函數(shù):錢包A調(diào)用,攜帶111 wei,參數(shù)為C合約地址,num=777。調(diào)用結(jié)果:
- B合約狀態(tài)變量被改變:num=777,sender=A錢包地址,value=111;
- 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的變量上。

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

注意:
和call不一樣,delegatecall在調(diào)用合約時可以指定交易發(fā)送的gas,但不能指定發(fā)送的ETH數(shù)額(value)。
delegatecall有安全隱患,使用時要保證當(dāng)前合約和目標(biāo)合約的狀態(tài)變量存儲結(jié)構(gòu)相同,并且目標(biāo)合約安全,不然會造成資產(chǎn)損失。
15.2.3 delegatecall使用場景
代理合約(
Proxy Contract):將智能合約的存儲合約和邏輯合約分開:代理合約(Proxy Contract)存儲所有相關(guān)的變量,并且保存邏輯合約的地址;所有函數(shù)存在邏輯合約(Logic Contract)里,通過delegatecall執(zhí)行。當(dāng)升級時,只需要將代理合約指向新的邏輯合約即可。EIP-2535 Diamonds(鉆石):鉆石是一個支持構(gòu)建可在生產(chǎn)中擴展的模塊化智能合約系統(tǒng)的標(biāo)準(zhǔn)。鉆石是具有多個實施合同的代理合同。 更多信息請查看:鉆石標(biāo)準(zhǔn)簡介。