智能合约-函数调用那些事


图片来源
本文对智能合约函数中常见的问题做一总结。内容涉及调用编码、调用过程、回退函数和内联汇编。

1. 函数调用的编码过程

在通过 ABI 接口进行合约调用时,需要对调用的数据进行编码和封装,赋值到 msg 的 data 字段中(data 不为空,代表存在合约调用)。
编码的内容,将涉及到函数签名和调用参数。
所谓函数签名,将对函数名和参数列表进行摘要(SHA3)计算,计算的结果为 4 个字节的数据,这部分数据又可以成为 “函数选择器”。
比如 transferFrom(address,address,uint256) 的函数选择器为 bytes4(keccak256(‘transferForm(address,address,uint256)’)) ,即 ‘0x23b872dd’。
也因此,编码后的参数排列在函数选择器的后面,从第 5 个字节开始。

参数的编码,其实就是参数的序列化过程。下面,我们以 GnosisSafeProxyFactory 中的 createProxyWithNonce 调用为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Function: createProxyWithNonce(address _singleton, bytes initializer, uint256 saltNonce)

MethodID: 0x1688f0b9
/// d9db270c1b5e3bd161e8c8503c55ceabee709552 是 GnosisSafe 合约
[0]: 000000000000000000000000d9db270c1b5e3bd161e8c8503c55ceabee709552
/// initializer 是从第 96 个字节开始(动态数据的位置偏移量)
[1]: 0000000000000000000000000000000000000000000000000000000000000060
/// saltNonce 的值是 '0x1813d227a48'(静态部分,直接表示)
[2]: 000000000000000000000000000000000000000000000000000001813d227a48
/// 第 96 个字节开始的地方。由于这是一个 bytes,动态数组,因此,0x1c4 代表数组大小 452。
[3]: 00000000000000000000000000000000000000000000000000000000000001c4
/// b63e800d 代表另外一个要调用的函数的选择器:
/// keccak256("setup(address[],uint256,address,bytes,address,address,uint256,address)")
[4]: b63e800d
/// 第一个参数 address[] 是从第 256 个字节开始的
0000000000000000000000000000000000000000000000000000000000000100
/// 至少需要 三个人 参与(阈值)
[5]: 0000000000000000000000000000000000000000000000000000000000000003
/// delegatecall 调用地址
[6]: 0000000000000000000000000000000000000000000000000000000000000000
/// 第四个参数 bytes 是从第 0x1a0,即 416 个字节开始的
[7]: 00000000000000000000000000000000000000000000000000000000000001a0
/// fallbackHandler Handler for fallback calls to this contract
[8]: 000000000000000000000000f48f2b2d2a534e402487b3ee7c18c33aec0fe5e4
/// paymentToken Token that should be used for the payment (0 is ETH)
[9]: 0000000000000000000000000000000000000000000000000000000000000000
/// payment Value that should be paid
[10]: 0000000000000000000000000000000000000000000000000000000000000000
/// paymentReceiver Address that should receive the payment (or 0 if tx.origin)
[11]: 0000000000000000000000000000000000000000000000000000000000000000
/// 第一个参数 address[] 真正开 始的地方。这里就是第 256 的位置。数组长度为 4.
[12]: 0000000000000000000000000000000000000000000000000000000000000004
/// address[0]
[13]: 000000000000000000000000792b18b5453b1e265bfe71ea33131155745200a3
/// address[1]
[14]: 0000000000000000000000005796e1051c18ed58d980ff935ce188a63d086e7a
/// address[2]
[15]: 000000000000000000000000a9227990e08aeb90e3538fbf6e629e8e545705e0
/// address[3]
[16]: 000000000000000000000000357ad87d4e546bb2406f020675db2f1e343fdda2
/// 第四个参数 bytes 真正开始的地方,即第 416 个字节开始的地方,后面一共 32+32-4 = 60 个字节。
/// Data payload for optional delegate call.
[17]: 0000000000000000000000000000000000000000000000000000000000000000
[18]: 00000000000000000000000000000000000000000000000000000000

2. call 和 delegatecall

call 和 delegatecall 均可以用于函数调用。但是,它们在函数调用过程中,msg.senderaddress(this) 存在巨大差异。
我们定义三个合约,如下:
Greeter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract Greeter {

string private greeting;

constructor(string memory _greeting) {
greeting = _greeting;
console.log("Deploying a Greeter at:", address(this));
}

function greet() public view returns (string memory) {
console.log("caller1:", msg.sender);
console.log("adthis1:", address(this));
return greeting;
}
}

Greeter2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.8.0;

import "./Greeter.sol";

contract Greeter2 {
Greeter private greeter ;
constructor(address addr){
greeter = Greeter(addr) ;
console.log("Deploying a Greeter2 at:", address(this));
}

function greet() public {
console.log("caller2:", msg.sender);
console.log("adthis2:", address(this));
(address(greeter)).delegatecall(abi.encodeWithSignature("greet()"));
}
}

Greeter3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.8.0;
import "./Greeter2.sol";
import "./Greeter.sol";

contract Greeter3 {
Greeter private greeter1 ;
Greeter2 private greeter2 ;

constructor(address addr1,address addr2){
greeter1 = Greeter(addr1) ;
greeter2 = Greeter2(addr2) ;
console.log("Deploying a Greeter3 at:", address(this));
}

function greet() public {
console.log("caller3:", msg.sender);
console.log("adthis3:", address(this));
(address(greeter2)).delegatecall(abi.encodeWithSignature("greet()"));
}
}

测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe("Greeter", function () {
it("Should return the new greeting once it's changed", async function () {
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, world!");
await greeter.deployed();

console.log('************************************')
console.log('-----------greater2-----------------')
const Greeter2 = await ethers.getContractFactory("Greeter2");
const greeter2 = await Greeter2.deploy(greeter.address);
await greeter2.deployed();

console.log('************************************')
console.log('-----------greater3-----------------')
const Greeter3 = await ethers.getContractFactory("Greeter3");
const greeter3 = await Greeter3.deploy(greeter.address,greeter2.address);
await greeter3.deployed();
await greeter3.greet() ;
});
});

获得结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
Deploying a Greeter at: 0x5fbdb2315678afecb367f032d93f642f64180aa3
************************************
-----------greater2-----------------
Deploying a Greeter2 at: 0xe7f1725e7734ce288f8367e1bb143e90bb3f0512
************************************
-----------greater3-----------------
Deploying a Greeter3 at: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
caller3: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
adthis3: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
caller2: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
adthis2: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
caller1: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
adthis1: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0

如上所示,可知:delegatecall 是可以 “传递” 的。它可以一直改变调用过程的 address(this) 和 msg.sender

对于 this 而言,表示调用执行所在的合约,因此,address(this) 则表示当前调用运行所在的合约地址。

鉴于 delegatecall 时,this 的传递性,因此,在进行这种调用时,可能会引发的 Storage Collision,前面的文章已经介绍,这里不在赘述。

如果在 Greeter2 中的 greet() 函数内部,实际使用了 call 调用,其结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Deploying a Greeter at: 0x5fbdb2315678afecb367f032d93f642f64180aa3
************************************
-----------greater2-----------------
Deploying a Greeter2 at: 0xe7f1725e7734ce288f8367e1bb143e90bb3f0512
************************************
-----------greater3-----------------
Deploying a Greeter3 at: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
caller3: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
adthis3: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
caller2: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
adthis2: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
caller1: 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
adthis1: 0x5fbdb2315678afecb367f032d93f642f64180aa3

如上可知,delegatecall 链条一旦中断,变成 call 调用,则,在进行调用时,被调用者中的 msg.sender 就变成了调用者调用时的 address(this),被调用者中的 address(this) 就是被调用者合约自身。


4. 回退函数

回退函数无函数名/无参数/无返回值。当合约中没有相应的方法匹配给调用者的时候,会调用到该函数中。

The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked 
payable.
官网

正因为存在此类回退函数,使得我们可以实现代理调用。
代理人在函数中使用 call 或者 delegatecall 来中继调用,将接收到的所有函数调用转发给真正的实现者。因此,这时,实现者是可以更换的,我们只是把代理暴露给了相关调用者,因此,这也就实现了所谓的 合约的 “可升级性”。

同时,借助回退函数和 delegatecall,还可以实现合约逻辑(代理实现者)和合约状态数据(代理)的分离,可以有效降低合约所需消耗。
由状态数据组成的合约,作为代理,在其 回退函数 中,delegatecall 真正的合约实现者。这样,每次部署时,只需要部署代理即可,所有的代理都可以指向同一个代理实现者。


4. 内联汇编

接着回退函数,我们来看看内联汇编的使用。不同于 Solidity,这是一种可以直接通过操作符和 EVM 交互的方式。

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function() payable public {
address _impl = implementation();
require(_impl != address(0));

assembly {
// 获取当前 0x40 所指向的空余 memory 所在的地址
let ptr := mload(0x40)
// 将 0 到 calldatasize 的 calldata 数据,拷贝到 ptr 所指向的 memory 空余空间
calldatacopy(ptr, 0, calldatasize)
// result 即执行是否成功
// delegatecall(g, a, in, insize, out, outsize)
// https://docs.soliditylang.org/en/latest/yul.html#yul-call-return-area
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
// 获取返回值的大小
let size := returndatasize
// 将 0 到 size 的 returndata 返回到 ptr 所指向的 memory 空余空间。注意,这里会覆盖 calldata,但是 calldata 已经没有用了。
returndatacopy(ptr, 0, size)

switch result
case 0 {revert(ptr, size)}
default {return (ptr, size)}
}
}

如上所示,内敛块使用 assembly {...} 包裹,使用 Yul 语言描述,一种更细粒度的编码方式,更加灵活,却也更加危险,是一种 low level 层面的调用。

参考链接

Inline Assembly

© 2024 YueGS