上一篇文章介绍了Solidity的继承、接口、抽象合约、库和异常处理等进阶语法;这一章准备介绍一些Solidity收发代码,合约交互的高级内容
接收 ETH
Solidity支持两种特殊的回调函数,receive()
和fallback()
,他们主要在两种情况下被使用:
- 接收ETH
- 处理合约中不存在的函数调用(代理合约proxy contract)
接收ETH函数 receive
- receive()只用于处理接收ETH。
- 一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。
- receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。
js
// 定义事件
receive() external payable {
// ...
}
回退函数 fallback
fallback函数充当了合约的默认处理函数,用于处理没有明确定义处理方式的消息。
fallback函数会在三种情况下被调用:
-
调用者尝试调用一个合约中不存在的函数时
-
用户给合约发Ether但是receive函数不存在
-
用户发Ether,receive存在,但是同时用户还发了别的数据(msg.data不为空)
fallback是solidity中的特殊函数,定义方式为fallback()关键字。需要注意的是fallback需要被定义为external
js
fallback() external payable virtual {
_fallback();
}
合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。
![](https://file.jishuzhan.net/article/1762773980344750082/546e839933a9cd2a4cbfe1f4a72b8057.webp)
发送 ETH
有三种方法向其他合约发送ETH
,他们是:transfer()
,send()
和call()
transfer
- 用法是
接收方地址.transfer(发送ETH数额)
。 transfer()
的gas
限制是2300
,足够用于转账,但对方合约的fallback()
或receive()
函数不能实现太复杂的逻辑。transfer()
如果转账失败,会自动revert
(回滚交易)。
JS
// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external payable{
_to.transfer(amount);
}
send
- 用法是
接收方地址.send(发送ETH数额)
。 send()
的gas
限制是2300
,足够用于转账,但对方合约的fallback()
或receive()
函数不能实现太复杂的逻辑。send()
如果转账失败,不会revert
。send()
的返回值是bool
,代表着转账成功或失败,需要额外代码处理一下。
js
// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
// 处理下send的返回值,如果失败,revert交易并发送error
bool success = _to.send(amount);
if(!success){
revert SendFailed();
}
}
call
- 用法是
接收方地址.call{value: 发送ETH数额}("")
。 call()
没有gas
限制,可以支持对方合约fallback()
或receive()
函数实现复杂逻辑。call()
如果转账失败,不会revert
。call()
的返回值是(bool, data)
,其中bool
代表着转账成功或失败,需要额外代码处理一下。
js
// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
// 处理下call的返回值,如果失败,revert交易并发送error
(bool success,) = _to.call{value: amount}("");
if(!success){
revert CallFailed();
}
}
- call没有gas限制,最为灵活,是最提倡的方法;
- transfer有2300 gas限制,但是发送失败会自动revert交易,是次优选择;
- send有2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。
合约之间的调用
先写一个OtherContract
合约,以供调用。CallContract
合约去调用OtherContract
合约
js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract OtherContract {
uint256 private _x = 0; // 状态变量_x
// 收到eth的事件,记录amount和gas
event Log(uint amount, uint gas);
// 返回合约ETH余额
function getBalance() view public returns(uint) {
// 用于获取当前合约地址的以太币余额
return address(this).balance;
}
// 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)
function setX(uint256 x) external payable{
_x = x;
// 如果转入ETH,则释放Log事件
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
// 读取_x
function getX() external view returns(uint x){
x = _x;
}
}
contract CallContract {
function callSetX(address _address,uint256 x) external{
OtherContract(_address).setX(x);
}
}
部署OtherContract
合约,可以获取到一个合约地址
![](https://file.jishuzhan.net/article/1762773980344750082/03434b975fd08db1e9f36a927cf9233c.webp)
再部署CallContract
合约,我们调用第一个合约里的方法 修改x 值
调用合约函数后,可以看到第一个合约里的数据已改变
![](https://file.jishuzhan.net/article/1762773980344750082/c556b1bab3e31373be6b2b1b3bf35640.webp)
call 和 delegatecall
call
- call是solidity官方推荐的通过触发fallback或receive函数发送ETH的方法。
- 不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数
call 是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data),分别对应call是否成功以及目标函数的返回值。
js
目标合约地址.call(二进制编码);
其中二进制编码利用结构化编码函数abi.encodeWithSignature
获得:
js
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
函数签名 为"函数名(逗号分隔的参数类型)"。例如:abi.encodeWithSignature("f(uint256,address)", _x, _addr)
另外call在调用合约时可以指定交易发送的ETH数额和gas:
js
目标合约地址.call{value:发送数额, gas:gas数额}(二进制编码);
举个例子:
js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract OtherContract {
uint256 private _x = 0; // 状态变量_x
// 收到eth的事件,记录amount和gas
event Log(uint amount, uint gas);
fallback() external payable{}
// 返回合约ETH余额
function getBalance() view public returns(uint) {
// 用于获取当前合约地址的以太币余额
return address(this).balance;
}
// 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)
function setX(uint256 x) external payable{
_x = x;
// 如果转入ETH,则释放Log事件
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
// 读取_x
function getX() external view returns(uint x){
x = _x;
}
}
contract CallContract {
// 定义Response事件,输出call返回的结果success和data
event Response(bool success, bytes data);
// 调用设置X函数
function callSetX(address payable _addr, uint256 x) public payable {
// call setX(),同时可以发送ETH
(bool success, bytes memory data) = _addr.call{value: msg.value}(
abi.encodeWithSignature("setX(uint256)", x)
);
emit Response(success, data); //释放事件
}
function callGetX(address _addr) external returns(uint256){
// call getX()
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("getX()")
);
emit Response(success, data); //释放事件
return abi.decode(data, (uint256));
}
// 调用不存在的函数
function callNonExist(address _addr) external{
// call getX()
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("foo(uint256)")
);
emit Response(success, data); //释放事件
}
}
先部署OtherContract
合约,获取合约地址,再部署CallContract
合约
![](https://file.jishuzhan.net/article/1762773980344750082/7d33418eba1d6ca173ad3ebd5b0bb5d8.webp)
调用callSetX
函数,打印的log信息可以看到成功状态:
![](https://file.jishuzhan.net/article/1762773980344750082/89fe58d8fee767bb359fe4b977830819.webp)
调用getX
函数,利用abi.decode
解码call的返回值,打印的log信息可以看到成功状态和返回值信息:
![](https://file.jishuzhan.net/article/1762773980344750082/b7b0dcffc1d37264319a8a1aca21d568.webp)
经过abi.decode
解码,0x000000000000000000000000000000000000000000000000000000000000000b
最终返回值为11
调用不存在的函数: call
输入的函数不存在于目标合约,那么目标合约的fallback
函数会被触发。
虽然call可以调用合约,但是并不推荐使用call调用,因为不安全
delegatecall
delegate
中是委托/代表的意思
delegatecall
使用方法和call
类似
js
目标合约地址.delegatecall(二进制编码);
二进制编码
利用结构化编码函数abi.encodeWithSignature
获得:
js
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
delegatecall
在调用合约时可以指定交易发送的gas
,但不能指定发送的ETH
数额
delegatecall主要有两个应用场景:
-
代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。
-
EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合同的代理合同。
js
// 通过call来调用C的setVars()函数,将改变合约C里的状态变量
function callSetVars(address _addr, uint _num) external payable{
// call setVars()
(bool success, bytes memory data) = _addr.delegatecall(
// 参数要指定字节
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
区别
虽然call
和delegatecall
用法差不多,但是他们还是有区别的
当用户A
通过合约B
来call
合约C
的时候:
执行的是合约C
的函数,语境
(Context
,可以理解为包含变量和状态的环境)也是合约C
的,msg.sender
是B
的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C
的变量上。
![](https://file.jishuzhan.net/article/1762773980344750082/c3aba1ddcb2ea678d616055393d149c2.webp)
当用户A
通过合约B
来delegatecall
合约C
的时候:
执行的是合约C
的函数,但是语境
仍是合约B
的,msg.sender
是A
的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B
的变量上。
![](https://file.jishuzhan.net/article/1762773980344750082/2e8d6e3bb6f144c82fe697e59b269858.webp)
合约中创建合约
有两种方法可以在合约中创建新合约,create
和create2
create
create
的用法很简单,就是new
一个合约,并传入新合约构造函数所需的参数:
js
Contract x = new Contract{value: _value}(params)
// 合约名字 x = new 合约名字{value: _value}(params)
- Contract是要创建的合约名字
_value
是创建时要转入的ETH数量params
是新合约构造函数的参数
create2
ABI
ABI (Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。在 EVM 处理数据时,所有的数据根据 ABI 标准进行编码。
Solidity
中,ABI编码
有4个函数:abi.encode
, abi.encodePacked
, abi.encodeWithSignature
, abi.encodeWithSelector
。
ABI解码
有1个函数:abi.decode
,用于解码abi.encode
的数据
abi.encode
ABI
被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果要和合约交互,就要用abi.encode
。
abi.encode
用于对给定的参数进行 ABI 编码,返回一个字节数组。
js
function encode() public view returns(bytes memory result) {
result = abi.encode(x, addr, name, array);
}
![](https://file.jishuzhan.net/article/1762773980344750082/f64bf2b5bad0aeea9d8e608dbe1978ea.webp)
abi.decode 解码
对于所有使用abi.encode
编码的内容,都可以使用abi.decode
解码。
第一个参数是编码数据的字节数组,第二个参数是解码后的数据类型。
js
address decodedAddress = abi.decode(encodedData, (address));
//多个参数
(uint256 decodedUint, address decodedAddress, string memory decodedString) = abi.decode(encodedData, (uint256, address, string));
abi.encodePacked
将给定参数根据其所需最低空间编码。它类似 abi.encode
,但是会把其中填充的很多0
省略。比如,只用1字节来编码uint
类型。当你想省空间,并且不与合约交互的时候,可以使用abi.encodePacked
,例如算一些数据的hash
时。
js
function encodePacked() public view returns(bytes memory result) {
result = abi.encodePacked(x, addr, name, array);
}
![](https://file.jishuzhan.net/article/1762773980344750082/0c041224066339e6514df9c8af61c9c9.webp)
abi.encodeWithSignature
与abi.encode
功能类似,只不过第一个参数为函数签名
,比如"foo(uint256,address)"
。当调用其他合约的时候可以使用。
js
function encodeWithSignature() public view returns(bytes memory result) {
result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}
![](https://file.jishuzhan.net/article/1762773980344750082/62e575a0e8dd79d9ba9ccabf459f02b8.webp)
abi.encodeWithSelector
与abi.encodeWithSignature
功能类似,只不过第一个参数为函数选择器
,为函数签名
Keccak哈希的前4个字节。
js
function encodeWithSelector() public view returns(bytes memory result) {
result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
}
![](https://file.jishuzhan.net/article/1762773980344750082/f3897c9dfabc5305f78339dfd3a86f4d.webp)