在智能合約的呼叫中我們必須知道一件事情那就是交易的原始發起者「必須是」一個 EOA,那如果我們在一個合約 A 中呼叫另外一個合約 B,要怎麼去決定 msg.sender
呢?
EOA -> Contract A -> Contract B
在以上的情況中,Contract B 的角色判斷如下:
Call Type | msg.sender | Context | update State | Usage |
---|---|---|---|---|
Call | A | B | Yes | 用於更新狀態(Write) |
Call Code | A | A | Yes | 已經不能用囉 |
Delegate Call | A's msg.sender | A | Yes | 取代 "call code"、連接 Library |
Static Call | A | B | NO | 單純查看狀態(View) |
.call
:
gas
限制<address>.call(bytes memory) returns (bool, bytes memory)
send
一樣,如果執行失敗不會停止而是 return false<address>.call(bytes memory) returns (bool, bytes memory)
<address>.call{value: amount}( bytes memory )
功能類似 .transfer 會從合約轉入帳號 amount 價值的 eth<address>.call{gas: amount}( bytes memory )
可以調整供給的 gas 數量,並且 returns 一個 boolean 值.delegatecall
:和 call 基本上一樣,只是使用 delegatecall 時不能使用 value 但可以附帶註明 gas.staticcall
:和 call 基本上一樣,只是使用 staticcall 時不能改變到 contract 中的任何狀態(static)我們可以從以下這個 EOA -> Contract A -> Contract B
的例子知道 call 是如何使用。
假設我們的合約是這樣:
contract ContractA {
ContractB immutable public B;
uint public aVal;
constructor (address B_addr) {
B = ContractB(B_addr);
}
function callB(bool _fail) external {
// 我們要在這裡動手腳
}
}
contract ContractB {
uint public bVal;
function fx(bool _fail) external returns (uint) {
require(!_fail, "Failed");
bVal = 1;
return 2;
}
}
在 callB()
中如果是正常的呼叫另外一個合約 B 的函式 fx()
:
function callB(bool _fail) external {
aVal = B.fx(_fail); // 我們要在這裡動手腳
}
則 EVM bytecode 作用如下:
...
PC: 0x117, opcode: JUMPDEST
PC: 0x118, opcode: POP
PC: 0x119, opcode: GAS
PC: 0x11a, opcode: CALL ─> aVal = B.fx(_fail);
PC: 0x11b, opcode: ISZERO ─> 如果 call 回傳 0 就會產生 error
PC: 0x11c, opcode: DUP1
PC: 0x11d, opcode: ISZERO ┐ result = "the call returned 0"
PC: 0x11e, opcode: PUSH2 0x012b ├─> if(result == 0) jump to 0x012b 並且 fetch aVal
PC: 0x121, opcode: JUMPI ┘
PC: 0x122, opcode: RETURNDATASIZE ┐
PC: 0x123, opcode: PUSH1 0x00 │ Otherwise forward the revert message.
PC: 0x125, opcode: DUP1 │
PC: 0x126, opcode: RETURNDATACOPY ├─> The revert message from ContractB is on the return data.
PC: 0x127, opcode: RETURNDATASIZE │
PC: 0x128, opcode: PUSH1 0x0 │ 複製 ContractB 回傳來的 data 到 memory.
PC: 0x12a, opcode: REVERT ┘ Push 到 stack
...
那如果我們是使用 call
在 callB()
中:
function callB(bool _fail) external {
bool success;
bytes memory returnValueEncoded;
bytes memory funcParams = abi.encodeWithSelector(conB.stuff1.selector, _fail);
(success, returnValueEncoded) = address(conB).call(funcParams);
if (success) {
val = abi.decode(returnValueEncoded, (uint256));
}
else {
// Assume a revert (and not a panic or user defined function)
assembly {
// Remove the function selector.
returnValueEncoded := add(returnValueEncoded, 0x04)
}
string memory revertReason = abi.decode(returnValueEncoded, (string));
revert(revertReason);
}
}
以上的第二個寫法我們就可以把 call
換成 delegateCall
或其他魔法,當然也可以考慮在這其中塞入更多動作讓這筆交易不是只有單純 cross contract call!這對 contractB 中有限定 msg.sender
或其他條件時有非常大的幫助。
calldata 這個詞在以太坊中有兩種意思,一個是交易的 tx data,也就是交易中的 data field;另外一個則是 Solidity 中的記憶體儲存方式(與 storage, memory 一夥),現在要講的是前者,而後者我們留待 EVM & Memory Pool 的部分講述。
當我們要發起一筆交易來與合約函式互動時,to
指的就是合約地址,而 data
也就是 calldata
,會包裹著 Function Selector 以及需要交付的參數。
例如我們要與 transfer(address _to, uint256 _value)
這個函式互動,他的 Function Selector(Method ID) 為 SHA-3 (or Keccak-256) 後取 4 bytes,則為:a9059cbb
。而我們要輸入的參數有兩個,如果分別為 0xB42faBF7BCAE8bc5E368716B568a6f8Fdf3F84ec
及 0x2a34892d36d6c74
,我們可以得到最後要使用的 calldata 為:0xa9059cbb000000000000000000000000B42faBF7BCAE8bc5E368716B568a6f8Fdf3F84ec000000000000000000000000000000000000000000000000002a34892d36d6c74
。那因為每個參數的長度為 32 bytes(也就是 64 hexadecimal characters),所以大家才會看見那麼多個 0。
每次在發動或查看各種 call
的結果時,總是和 Transaction 脫不了關係,如果想要更深入了解的話記得要關注這次的系列文,最後幾天會有詳細的介紹!
關於 Cross Contract Call 我們可以延伸閱讀可以把 interface 轉為 abstract contract 的 ERC-165。
最後歡迎大家拍打餵食大學生
0x2b83c71A59b926137D3E1f37EF20394d0495d72d