Solidity 学习 -练气期(十三)

ABI 的编码和解码

ABI(application binary interface,应用二进制接口) 是与以太坊智能合约交互的标准。
数据基于他们的类型编码,并且由于编码后不包含类型信息,解码事需要注明他们的类型。
编码函数:
(1)abi.encode
(2)abi.encodePacked
(3)abi.encodeWithSignature
(4)abi.encodeWithSelector
abi.decode可以解码abi.encode编码的数据。

准备4个类型变量,uint256,address,string,uint256[2]

  1. abi.encode
    ABI 被设计来和智能合约交互,它将每个参数填充32字节的数据并拼接在一起。与合约交互手段就是abi.encode
    function encode() public view returns(bytes memory result) {
        result = abi.encode(x, addr, name, array);
    }

编码结果
0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
每个数据都用0填充补充道32字节

  1. abi.encodePacked
    function encodePacked() public view returns(bytes memory result) {
        result = abi.encodePacked(x, addr, name, array);
    }

0x000000000000000000000000000000000000000000000000000000000000000a5b38da6a701c568545dcfcb03fcb875f56beddc43078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006
将给定参数根据其所需要的最低空间编码。类似abi.encode会把其中填充的0省略。
比如只用一个字节编码uint8 类型。如果需要节省空间不需要与合约交互,可以使用它。比如计算一些数据的hash时。

  1. abi.encodeWithSignature
    function encodeWithSignature() public view returns(bytes memory result) {
        result = abi.encodeWithSignature(
            "foo(uint256,address,string,uint256[2])", 
            x, addr, name, array);
    }

0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
相当于在abi.encode的编码结果加了4个字节的选择器

  1. abi.encodeWithSelector
    function encodeWithSelector() public view returns(bytes memory result) {
        result = abi.encodeWithSelector(
            bytes4(keccak256("foo(uint256,address,string,uint256[2])")), 
            x, addr, name, array);
    }

0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
编码结果月abi.encodeWithSignature编码结果一致

ABI 解码

    function decode(bytes memory data) public pure 
        returns(uint dx,address daddr, string memory dname, uint[2] memory darray) {
            (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
    }

用abi.encode的结果作为参数,进行解码。可以还原。

使用场景

(1)在合约开发中,ABI场配合call来实现对合约的底层调用。

        bytes4 selector = contract.getValue.selector;
        bytes memory data = abi.encodeWithSelector(selector, _x);
        (bool success, bytes memory returnedData) =
            address(contract).staticcall(data);
        require(success);
        return abi.decode(returnedData, (uint256))

(2) ethers.js 常用ABI实现合约的导入和函数调用

    const wavePortalContract = new ethers.Contract(
        contractAddress,contractABI, signer
    );
    const waves = await wavePortalContract.getAllWaves();

(3)对不开源合约进行反编译后,某些函数无法查询函数签名,可以通过ABI调用。
智能看到

    function 0x533ba33a() public payable {
        return stor_14;
    }

这种情况通过ABI函数选择器来调用函数

        bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a));
        (bool success, bytes memory returnedData) =
            address(contract).staticcall(data);
        require(success);
        return abi.decode(returnedData, (uint256))

哈希函数

哈希函数是一个密码学概念,它可以讲一个任意长度的消息,转换为一个固定长度的值,称作hash值

哈希函数的性质和应用

(1)单项性:从输入的消息道它的哈希值的正向运算,简答且唯一确定。而反过来非常难,只能暴力枚举。
(2)灵敏性:对输入消息改动微小,得到的哈希值改变很大
(3)高效性:从输入的消息到哈希的运算高效。
(4)均一性:每个哈希值被取到的概率应该基本相等
(5)抗碰撞性:
弱抗碰撞性:给定一个消息x,找到另一个消息x‘ 使得hash(x) = hash(x’)是困难的
强抗碰撞性:找到任意一对不同的消息x和x‘ ,是的hash(x) = hash(x’)是困难的

应用于现代计算机应用,包括生成唯一标识,数字签名,华联网安全加密(SSL/TLS)等

keccak256函数

Solidity 中最常用的哈希函数keccak256,基于keccak算法
hash = keccak256(data);
输入参数data时bytes类型的数据,返回hash 是bytes32类型的哈希值

  1. Keccak算法和SHA-3算法
    SHA-3算法基于Keccak算法标准化而来。很多场合Keccak算法和SHA-3算法同义
    Solidity0.5.0版本移除了SHA-3算法,现在直接使用keccak256

  2. 生成数据唯一的标识

  3. 弱抗碰撞

  4. 强抗碰撞

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract UseHash {
    //生成数据唯一的标识
    function hash(uint _num, string memory _string, address _addr)
        public pure returns(bytes32) {
            return keccak256(abi.encodePacked(_num, _string ,_addr));
        }
    //弱抗碰撞
    bytes32 _msg = keccak256(abi.encodePacked("0xAA"));
    function weak(string memory string1) public view returns (bool) {
        return keccak256(abi.encodePacked(string1)) == _msg;
    }
    //强抗碰撞
    function strong(string memory string1,string memory string2) public pure returns (bool) {
        return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2));
    }
}
image.png

函数选择器

我们调用智能合约时,本质上是想目标合约发送了一段calldata类型的数据。

  1. msg.data

msg.data 是Solidity中的一个全局变量,它的值为calldata,也就是调用函数时传入的数据。
借助代码输出调用函数的calldata:
地址参数输入;0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
输出的calldata:0x6a6278420000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4

    event Log(bytes data);

    function mint(address to) external {
        emit Log(msg.data);
    }

(1)开头4个字节选择器 0x6a627842
(2)后面32个字节为输入的参数,其中前面12个字节为用来填充0,后面20个字节为地址参数to:
0x 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
calldata就是在告诉智能合约,用户想要调用那个函数,参数是什么。

  1. method id、函数选择器和函数签名

method id定义为函数签名经过keccak256函数计算后的哈希值前面4个字节。当函数选择器和method id相匹配,即表示调用该函数。

函数签名,前面说过,“函数名(逗号分隔的参数类型)” 例如:mint 函数签名“mint(address)”.同一个智能合约不同的函数签名不同,因此我们通过函数签名来确定调用那个函数。
注意⚠️:在函数签名中uint和int 要写成uint256和int256
验证函数签名:

    function minSelector() external pure returns(bytes4 mSelector) {
        return bytes4(keccak256("mint(address)"));
    }

验证结果一致:0x6a627842


image.png

image.png
  1. 使用函数选择器调用函数
    function callWithSignature() external returns (bool, bytes memory) {
        (bool success, bytes memory data) = 
            address(this).call(abi.encodeWithSelector(
                0x6a627842, 
                0x005b38da6a701c568545dcfcb03fcb875f56beddc4));
            return (success, data);
    }

mint调用成功输出Log事件

捕获异常

程序遇到异常则会中断运行并报错,并非所有异常都需要中断运行,某些可以通过主动捕获并处理,不影响到整个程序运行。
try-catch是现代编程语言都具备的标准方式。solidity 6.0版本也添加了关键字

solidity中,try-catch 智能被用在声明为external 的函数或合约的构造函数constructor(被自动声明为external的函数) 中。语法

try externalContract.f() {
} catch {
}

externalContract.f() 是某个外部合约的函数调用,try模块在调用的情况下执行,catch模块调用失败跑出异常时执行。
在合约本身的内部可以this.f() 来替代externalContract.f() ,this.f() 也被视为外部调用。这种用法不可在构造函数中使用,因为此时还没有创建。

如果调用的函数有返回值,那么必须在try之后声明返回值returns(returnType val).并且在try模块可以使用返回的变量;如果是创建合约,那么返回值是新创建的合约变量。

try externalContract.f() returns (returnType val)  {
} catch {
}

catch 模块支持捕获异常原因

      try externalContract.f() returns (returnType)  {
        
        } catch Error(string memory){
        
        } catch Panic(uint) {
            
        } catch (bytes memeory) {
            
        }

try-catch 实战

  1. OnlyEvent合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract OnlyEvent {
    constructor(uint a) {
        require(a != 0,"invalid number");
        assert(a != 1);
    }

    function onlyEvent(uint256 b) external pure returns (bool success) {
        require(b % 2 ==0,"Ups! Reverting");
        success = true;
    }
}

OnlyEvent 包含一个构造函数,和一个onlyEvent函数
(1)构造函数有一个参数a,当a相当于0时,require语句会抛出异常;当a等与1时,assert会抛出异常;其他情况均正常。
(2)onlyEvent 函数有一个参数,允许b为偶数。当b为奇数时,require语句会抛出异常。

  1. 处理外部函数调用的异常、合约创建中的异常
    创建一个TryCatch合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "./OnlyEvent.sol";

contract TryCatch {
    event SuccessEvent();
    event CatchEvent(string message);
    event CatchByte(bytes data);

    OnlyEvent even;
    constructor() {
        even = new OnlyEvent(2);
    }

    // 在 external call 中使用 try-catch
    function execute(uint amount) external returns (bool success) {
        try even.onlyEvent(amount) returns(bool _success) {
            emit SuccessEvent();
            return _success;
        } catch Error(string memory reason) {
            emit CatchEvent(reason);
        }
    }

    //在创建新合约中使用try-catch (合约创建视为external call)
    // executeNew(0) 会失败释放  `CatchEvent`
    // executeNew(1) 会失败释放  `CatchByte`
    // executeNew(2) 会失败释放  `CatchEvent`
    function executeNew(uint a) external returns (bool success) {
        try new OnlyEvent(a) returns (OnlyEvent _even) {
            emit SuccessEvent();
            return _even.onlyEvent(a);
        } catch Error(string memory reason) {
            emit CatchEvent(reason);
        } catch (bytes memory reason) {
            emit CatchByte(reason);
        }
    }

}

SuccessEvent 是调用成功时释放的事件,而CatchEvent和CatchByte是抛出异常时事件,分别对应require/revert 和 assert 异常情况。event 是个OnlyEvent合约类型的状态变量。构造函数中,我们创建OnlyEvent合约时将参数设置为2,使得创建时不抛出异常。

在execute函数中使用try-catch处理调用外部函数onlyEvent中的异常

在remix上部署OnlyEvent和TryCatch合约,然后测试结果
execute(0) 的时候因为0是偶数,满足onlyEvent函数require的命令条件,多亿调用成功,没有异常抛出来,并释放SuccessEvent事件

execute(1),奇数不满足require命令,调用失败跑出异常被catch捕获,并释放CatchEvent事件

在execute改进处理合约常见时候异常。把try模块改成OnlyEvent合约创建即可

测试
executeNew(0) 会失败释放 CatchEvent
executeNew(1) 会失败释放 CatchByte
executeNew(2) 会失败释放 CatchEvent

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容