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]
- abi.encode
ABI 被设计来和智能合约交互,它将每个参数填充32字节的数据并拼接在一起。与合约交互手段就是abi.encode
function encode() public view returns(bytes memory result) {
result = abi.encode(x, addr, name, array);
}
编码结果
0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
每个数据都用0填充补充道32字节
- abi.encodePacked
function encodePacked() public view returns(bytes memory result) {
result = abi.encodePacked(x, addr, name, array);
}
0x000000000000000000000000000000000000000000000000000000000000000a5b38da6a701c568545dcfcb03fcb875f56beddc43078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006
将给定参数根据其所需要的最低空间编码。类似abi.encode会把其中填充的0省略。
比如只用一个字节编码uint8 类型。如果需要节省空间不需要与合约交互,可以使用它。比如计算一些数据的hash时。
- 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个字节的选择器
- 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类型的哈希值
Keccak算法和SHA-3算法
SHA-3算法基于Keccak算法标准化而来。很多场合Keccak算法和SHA-3算法同义
Solidity0.5.0版本移除了SHA-3算法,现在直接使用keccak256生成数据唯一的标识
弱抗碰撞
强抗碰撞
// 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));
}
}

函数选择器
我们调用智能合约时,本质上是想目标合约发送了一段calldata类型的数据。
- 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就是在告诉智能合约,用户想要调用那个函数,参数是什么。
- 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


- 使用函数选择器调用函数
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 实战
- 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语句会抛出异常。
- 处理外部函数调用的异常、合约创建中的异常
创建一个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