数据的编码与解码
abi.encode
:将给定的参数按照 ABI(应用二进制接口)规则编码成字节数组。编码后的数据总是 32 字节的倍数,不足 32 字节的部分会自动填充。适用于合约调用和数据传输。abi.encodePacked
:与abi.encode
类似。不同的是,它生成的字节数组是压缩过的,不会自动填充到 32 字节的倍数。适用于生成紧凑数据和哈希计算。abi.decode
:将abi.encode
返回的字节数组解码成原始数据。需要提供数据的类型信息。适用于解析从其他合约接收到的数据。
contract ABIExample {
uint num = 1;
address addr = 0x1234567890123456789012345678901234567890;
string str = "Hello, World!";
function encodeData() external view returns (bytes memory) {
return abi.encode(num, addr, str);
}
function decodePackedData() external view returns (bytes memory) {
return abi.encodePacked(num, addr, str);
}
function decodeData(
bytes memory data
) external pure returns (uint, address, string memory) {
(uint a, address b, string memory c) = abi.decode(
data,
(uint, address, string)
);
return (a, b, c);
}
}
-
部署 ABIExample 合约
-
调用 encodeData 方法,获取 num、addr、str 的编码数据
-
调用 decodePackedData 方法,获取 num、addr、str 的压缩编码数据,可以看到数据是紧凑的
-
传入步骤 2 中获取的编码数据,调用 decodeData 方法,解码数据,可以看到 num、addr、str 的原始数据
函数指针、函数签名、函数选择器
函数指针、函数签名、函数选择器均用于标识函数。
函数指针:指向函数的指针变量,可以用来调用函数
contract Demo {
uint public num;
string public str;
function update(
uint _num,
string calldata _str
) public returns (uint, string memory) {
num += _num;
str = _str;
return (num, str);
}
}
contract Test {
event Log(uint num, string str);
function callDemoUpdate(
address _demo,
uint _num,
string calldata _str
) public {
// 通过函数指针调用 Demo 合约的 update 函数
(uint num, string memory str) = Demo(_demo).update(_num, _str);
emit Log(num, str);
}
}
-
部署 Demo 合约、Test 合约
-
传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),调用 Test 合约的 callDemoUpdate 方法
-
查看 Demo 合约的 num、str 值,可以看到 num 值加 10、str 值为 “Hello”
-
查看 Log 事件,可以看到 Demo 合约的 num、str 值
函数签名:由函数名称和参数类型组成的字符串
上例的函数签名为 update(uint256,string)
,不能有空格、不能用简写。
函数选择器:函数签名的前 4 个字节
获取方法 1:bytes4(keccak256("update(uint256,string)"))
获取方法 2:Demo(_demo).update.selector
call 方法 & 函数调用数据
call
方法是一个低级函数,能通过 “函数调用数据” 与其他合约进行交互。
有 3 个方法获取 “函数调用数据”:
abi.encodeCall
将函数指针和参数编码成字节数组,并进行参数类型检查abi.encodeWithSignature
:将函数签名和参数编码成字节数组abi.encodeWithSelector
:将函数选择器和参数编码成字节数组
以上 3 种方法返回的字节数组即 “函数调用数据”。
contract Demo {
uint public num;
string public str;
function update(
uint _count,
string calldata _str
) public returns (uint, string memory) {
num += _count;
str = _str;
return (num, str);
}
}
contract Test {
event Log(uint num, string str);
function callDemoUpdateByPointer(
address _demo,
uint _count,
string calldata _str
) public {
// call 搭配 encodeCall 调用 Demo 合约的 update 函数
(bool success, bytes memory data) = _demo.call(
abi.encodeCall(Demo(_demo).update, (_count, _str))
);
require(success, "call failed");
(uint num, string memory str) = abi.decode(data, (uint, string));
emit Log(num, str);
}
function callDemoUpdateBySignature(
address _demo,
uint _count,
string calldata _str
) public {
// call 搭配 encodeWithSignature 调用 Demo 合约的 update 函数
(bool success, bytes memory data) = _demo.call(
abi.encodeWithSignature("update(uint256,string)", _count, _str)
);
require(success, "call failed");
(uint num, string memory str) = abi.decode(data, (uint, string));
emit Log(num, str);
}
function callDemoUpdateBySelector(
address _demo,
uint _count,
string calldata _str
) public {
bytes4 selector1 = bytes4(keccak256("update(uint256,string)"));
bytes4 selector2 = Demo(_demo).update.selector;
// call 搭配 encodeWithSelector 调用 Demo 合约的 update 函数
(bool success, bytes memory data) = _demo.call(
abi.encodeWithSelector(selector1, _count, _str)
);
require(success, "call failed");
(uint num, string memory str) = abi.decode(data, (uint, string));
emit Log(num, str);
}
}
-
部署 Demo 合约、Test 合约
-
传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),调用 Test 合约的 callDemoUpdateByPointer / callDemoUpdateBySignature / callDemoUpdateBySelector 方法
-
查看 Demo 合约的 num、str 值,可以看到 num 值加 10、str 值为 “Hello”
-
查看 Log 事件,可以看到 Demo 合约的 num、str 值
调用函数并传输以太
现有如下合约:
contract Demo {
uint public balance;
uint public num;
string public str;
function update(
uint _num,
string calldata _str
) public payable returns (uint, string memory) {
balance += msg.value;
num += _num;
str = _str;
return (num, str);
}
}
用函数指针调用方法时:
contract Test {
event Log(uint num, string str);
function callDemoUpdate(
address _demo,
uint _num,
string calldata _str
) public payable {
// 通过函数指针调用 Demo 合约的 update 方法并传输以太币
// 配置项 value 为传输的以太币数量下限, 这里设置为 msg.value
(uint num, string memory str) = Demo(_demo).update{value: msg.value}(
_num,
_str
);
emit Log(num, str);
}
}
-
部署 Demo 合约、Test 合约
-
传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),设置以太币数量(这里设置为 100),调用 Test 合约的 callDemoUpdate 方法
-
查看 Demo 合约的 balance、num、str 值,可以看到 balance 值加 100、num 值加 10、str 值为 “Hello”
-
查看 Log 事件,可以看到 Demo 合约的 num、str 值
用 call 调用方法时:
contract Test {
event Log(uint num, string str);
function callDemoUpdateBySignature(
address _demo,
uint _num,
string calldata _str
) public payable {
// 通过 call 调用 Demo 合约的 update 函数并传输以太币
(bool success, bytes memory data) = _demo.call{
value: 200, // 配置项 value 为传输的以太币数量下限, 这里设置为 200
gas: 500000 // 配置项 gas 为消耗的 gas 上限, 这里设置为 500000
}(abi.encodeWithSignature("update(uint256,string)", _num, _str));
require(success, "call failed");
(uint num, string memory str) = abi.decode(data, (uint, string));
emit Log(num, str);
}
}
-
部署 Demo 合约、Test 合约
-
传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),设置以太币数量(这里设置为 200),调用 Test 合约的 callDemoUpdateBySignature 方法
-
查看 Demo 合约的 balance、num、str 值,可以看到 balance 值加 200、num 值加 10、str 值为 “Hello”
-
查看 Log 事件,可以看到 Demo 合约的 num、str 值
Abstract & Interface
abstract contract (抽象合约):如果一个智能合约里面有未实现的函数 (缺少主体 { ... }
),则必须将该合约标为 abstract
。另外,未实现的函数需要标为 virtual
,以便子合约重写。
interface (接口):类似于抽象合约,但它不实现任何功能。
接口的规则:
-
不能包含状态变量
-
不能包含构造函数
-
不能继承除接口外的其他合约
-
所有函数都必须是 external 且不能有函数体
-
继承接口的非抽象合约必须实现接口定义的所有功能
我们以 ERC721 的接口合约 IERC721 为例,它定义了 3 个 event 和 9 个 function:
interface IERC721 is IERC165 {
// 在转账时被释放, 记录代币的发出地址 from、接收地址 to 和 tokenId
event Transfer(
address indexed from,
address indexed to,
uint256 indexed tokenId
);
// 在授权时被释放, 记录授权地址 owner、被授权地址 approved 和 tokenId
event Approval(
address indexed owner,
address indexed approved,
uint256 indexed tokenId
);
// 在批量授权时被释放, 记录批量授权的发出地址 owner、被授权地址 operator 和授权与否的 approved
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
// 返回某地址的 NFT 持有量 balance
function balanceOf(address owner) external view returns (uint256 balance);
// 返回某 tokenId 的主人 owner
function ownerOf(uint256 tokenId) external view returns (address owner);
// 授权另一个地址使用你的 NFT, 参数为被授权地址 approve 和 tokenId
function approve(address to, uint256 tokenId) external;
// 查询 tokenId 被批准给了哪个地址
function getApproved(
uint256 tokenId
) external view returns (address operator);
// 将自己持有的该系列 NFT 批量授权给某个地址 operator
function setApprovalForAll(address operator, bool _approved) external;
// 查询某地址的 NFT 是否批量授权给了另一个 operator 地址
function isApprovedForAll(
address owner,
address operator
) external view returns (bool);
// 普通转账, 参数为转出地址 from、接收地址 to 和 tokenId
function transferFrom(address from, address to, uint256 tokenId) external;
// 安全转账 (如果接收方是合约地址, 会要求实现 ERC721Receiver 接口), 参数为转出地址 from、接收地址 to 和 tokenId
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
// 安全转账的重载函数, 参数里面包含了 data
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
}
如果我们知道一个合约实现了 IERC721 接口,我们不需要知道它具体代码实现,就可以与它交互。
无聊猿 BAYC 属于 ERC721 代币,实现了 IERC721 接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用 IERC721 接口就可以与它交互,比如用 balanceOf 来查询某个地址的 BAYC 余额,用 safeTransferFrom 来转账 BAYC 。
contract interactBAYC {
// 利用 BAYC 地址创建接口合约变量 (ETH 主网)
IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);
// 通过接口调用 BAYC 的 balanceOf 查询持仓量
function balanceOfBAYC(
address owner
) external view returns (uint256 balance) {
return BAYC.balanceOf(owner);
}
// 通过接口调用 BAYC 的 safeTransferFrom 安全转账
function safeTransferFromBAYC(
address from,
address to,
uint256 tokenId
) external {
BAYC.safeTransferFrom(from, to, tokenId);
}
}
接口 ID:用于唯一标识接口。
接口 ID 是接口中所有函数选择器的异或(XOR)运算结果。具体步骤如下:
-
计算接口中每个函数的选择器(函数签名的 Keccak-256 哈希的前 4 字节)
-
对所有函数选择器执行异或运算
计算示例:
interface Solidity101 {
function hello() external pure;
function world(int) external pure;
}
contract Selector {
function calculateSelector() public pure returns (bytes4) {
Solidity101 i;
return i.hello.selector ^ i.world.selector;
}
}