保姆级Solidity教程三:高级语法,合约交互

上一篇文章介绍了Solidity的继承、接口、抽象合约、库和异常处理等进阶语法;这一章准备介绍一些Solidity收发代码,合约交互的高级内容

接收 ETH

Solidity支持两种特殊的回调函数,receive()fallback(),他们主要在两种情况下被使用:

  1. 接收ETH
  2. 处理合约中不存在的函数调用(代理合约proxy contract)

接收ETH函数 receive

  • receive()只用于处理接收ETH。
  • 一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。
  • receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。
js 复制代码
    // 定义事件
    receive() external payable {
       // ...
    }

回退函数 fallback

fallback函数充当了合约的默认处理函数,用于处理没有明确定义处理方式的消息。

fallback函数会在三种情况下被调用:

  1. 调用者尝试调用一个合约中不存在的函数时

  2. 用户给合约发Ether但是receive函数不存在

  3. 用户发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。

发送 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合约,可以获取到一个合约地址

再部署CallContract合约,我们调用第一个合约里的方法 修改x 值

调用合约函数后,可以看到第一个合约里的数据已改变

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合约

调用callSetX函数,打印的log信息可以看到成功状态:

调用getX函数,利用abi.decode解码call的返回值,打印的log信息可以看到成功状态和返回值信息:

经过abi.decode解码,0x000000000000000000000000000000000000000000000000000000000000000b最终返回值为11

调用不存在的函数: call输入的函数不存在于目标合约,那么目标合约的fallback函数会被触发。

虽然call可以调用合约,但是并不推荐使用call调用,因为不安全

delegatecall

delegate中是委托/代表的意思

delegatecall使用方法和call类似

js 复制代码
目标合约地址.delegatecall(二进制编码);

二进制编码利用结构化编码函数abi.encodeWithSignature获得:

js 复制代码
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)

delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额

delegatecall主要有两个应用场景:

  1. 代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。

  2. 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)
        );
    }

区别

虽然calldelegatecall用法差不多,但是他们还是有区别的

当用户A通过合约Bcall合约C的时候:

执行的是合约C的函数,语境(Context,可以理解为包含变量和状态的环境)也是合约C的,msg.senderB的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上。

当用户A通过合约Bdelegatecall合约C的时候:

执行的是合约C的函数,但是语境仍是合约B的,msg.senderA的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。

合约中创建合约

有两种方法可以在合约中创建新合约,createcreate2

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);
    }

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);
    }

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);
    }

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);
    }
相关推荐
Joker时代5 小时前
Anubi WebKey开启去中心化数字革命的新纪元
去中心化·区块链
软件工程小施同学5 小时前
区块链论文速读A会-ISSTA 2023(2/2)如何检测DeFi协议中的价格操纵漏洞
区块链·区块链会议·区块链论文
mutourend7 小时前
EVM-MLIR:以MLIR编写的EVM
区块链
虫小宝16 小时前
如何在Java中实现智能合约与区块链集成
java·区块链·智能合约
sino_sound17 小时前
从资金管理的角度 谈谈伦敦金投资技巧
区块链
UI设计开发服务商19 小时前
HMI 的 UI 风格成就经典
大数据·人工智能·数据分析·云计算·区块链
凄戚1 天前
分布式共识算法
分布式·区块链·共识算法
软件工程小施同学2 天前
区块链可投会议CCF C--TrustCom 2024 截止9.1 附去年录用文章
区块链·区块链会议·区块链论文·区块链投稿
Sui_Network2 天前
探索Sui的面向对象模型和Move编程语言
大数据·人工智能·学习·区块链·智能合约
Code blocks2 天前
小试牛刀-Solana合约账户详解
区块链·智能合约