图片来源 本文对智能合约函数中常见的问题做一总结。内容涉及调用编码、调用过程、回退函数和内联汇编。
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 [0 ]: 000000000000000000000000d9db270c1b5e3bd161e8c8503c55ceabee709552 [1 ]: 0000000000000000000000000000000000000000000000000000000000000060 [2 ]: 000000000000000000000000000000000000000000000000000001813d227a48 [3 ]: 00000000000000000000000000000000000000000000000000000000000001c4 [4 ]: b63e800d 0000000000000000000000000000000000000000000000000000000000000100 [5 ]: 0000000000000000000000000000000000000000000000000000000000000003 [6 ]: 0000000000000000000000000000000000000000000000000000000000000000 [7 ]: 00000000000000000000000000000000000000000000000000000000000001a0 [8 ]: 000000000000000000000000f48f2b2d2a534e402487b3ee7c18c33aec0fe5e4 [9 ]: 0000000000000000000000000000000000000000000000000000000000000000 [10 ]: 0000000000000000000000000000000000000000000000000000000000000000 [11 ]: 0000000000000000000000000000000000000000000000000000000000000000 [12 ]: 0000000000000000000000000000000000000000000000000000000000000004 [13 ]: 000000000000000000000000792b18b5453b1e265bfe71ea33131155745200a3 [14 ]: 0000000000000000000000005796e1051c18ed58d980ff935ce188a63d086e7a [15 ]: 000000000000000000000000a9227990e08aeb90e3538fbf6e629e8e545705e0 [16 ]: 000000000000000000000000357ad87d4e546bb2406f020675db2f1e343fdda2 [17 ]: 0000000000000000000000000000000000000000000000000000000000000000 [18 ]: 00000000000000000000000000000000000000000000000000000000
2. call 和 delegatecall call 和 delegatecall 均可以用于函数调用。但是,它们在函数调用过程中,msg.sender
和 address(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 { let ptr := mload (0x40 ) calldatacopy (ptr, 0 , calldatasize) let result := delegatecall (gas, _impl, ptr, calldatasize, 0 , 0 ) let size := returndatasize returndatacopy (ptr, 0 , size) switch result case 0 {revert (ptr, size)} default {return (ptr, size)} } }
如上所示,内敛块使用 assembly {...}
包裹,使用 Yul 语言描述,一种更细粒度的编码方式,更加灵活,却也更加危险,是一种 low level
层面的调用。
参考链接 Inline Assembly