数据的编码与解码
数据编码:
-
abi.encode:每个参数都会被填充为 32 字节的数据,并拼接在一起
-
abi.encodePacked:类似 abi.encode,但会省略其中填充的零
解码:
- abi.decode:接受两个参数:编码后的数据和类型列表
solidity
contract ABIExample {
function encodeData() external pure returns (bytes memory) {
uint a = 1;
address b = 0x1234567890123456789012345678901234567890;
string memory c = "Hello, World!";
return abi.encode(a, b, c);
}
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);
}
}
函数签名 & 函数选择器
solidity
function transfer(address recipient, uint amount) external returns (bool);
函数签名 :由函数名称和参数类型组成,中间没有空格 - transfer(address,uint)
函数选择器:
- 为函数签名通过 Keccak-256 哈希计算得到的前 4 个字节 -
bytes4(keccak256("transfer(address,uint)"))
- 如果不想手动编写函数签名,可以使用 Solidity 的内置函数
this.functionName.selector
来直接获取函数选择器
函数的编码
-
abi.encodeWithSignature:第一个参数为函数签名,后面的参数为函数参数
-
abi.encodeWithSelector:第一个参数为函数选择器,后面的参数为函数参数
solidity
contract TargetContract {
uint public value;
string public message;
function setValue(uint _value, string memory _message) public {
value = _value;
message = _message;
}
}
contract CallerContract {
function callSetValue1(
address target,
uint _value,
string memory _message
) public {
// 获取函数签名
string memory signature = "setValue(uint256,string)";
// 通过 encodeWithSignature 编码函数签名及传入的参数
(bool success, ) = target.call(
abi.encodeWithSignature(signature, _value, _message)
);
require(success, "Call failed");
}
function callSetValue2(
address target,
uint _value,
string memory _message
) public {
// 函数选择器 (方法 1)
bytes4 selector1 = bytes4(keccak256("setValue(uint256,string)"));
// 函数选择器 (方法 2)
bytes4 selector2 = TargetContract(target).setValue.selector;
// 通过 encodeWithSelector 编码函数选择器及传入的参数
(bool success, ) = target.call(
abi.encodeWithSelector(selector1, _value, _message)
);
require(success, "Call failed");
}
}
直接调用其他合约的方法
solidity
contract Demo1 {
// 方法 1: 通过地址调用
function setDemo2X_1(address _demo2, uint _x) public {
Demo2 demo2 = Demo2(_demo2);
demo2.setX(_x);
}
// 方法 2: 通过合约实例调用
function setDemo2X_2(Demo2 _demo2, uint _x) public {
_demo2.setX(_x);
}
}
contract Demo2 {
uint public x;
event Log(address caller, uint x);
function setX(uint _x) public {
x = _x;
emit Log(msg.sender, x);
}
}
-
部署 Demo2 合约
-
调用 Demo2 合约的 setX 方法,设置 x 值;查看 Demo2 合约的 x 值,可以看到 x 值被更新;查看 Log 事件,可以看到调用者地址为编辑器地址
-
部署 Demo1 合约
-
传入 Demo1 合约的地址和新 x 值,调用 Demo1 合约的 setDemo2X_1 方法;查看 Demo2 合约的 x 值,可以看到 x 值被更新;查看 Log 事件,可以看到调用者地址为 Demo1 合约地址
-
传入 Demo1 合约的地址和新 x 值,调用 Demo1 合约的 setDemo2X_2 方法;查看 Demo2 合约的 x 值,可以看到 x 值被更新;查看 Log 事件,可以看到调用者地址为 Demo1 合约地址
可以在调用的同时传输以太币:
solidity
contract Demo1 {
function setDemo2X(Demo2 _demo2, uint _x) public payable {
_demo2.setX{value: msg.value}(_x); // 要求: msg.value >= value 值; 这里设置成一样
}
}
contract Demo2 {
uint public x;
uint public value;
address public caller;
function setX(uint _x) public payable {
value = msg.value;
caller = msg.sender;
x = _x;
}
}
-
部署 Demo2 合约
-
传入新 x 值,设置以太币数量,调用 Demo2 合约的 setX 方法;查看 Demo2 合约的 x 值、value 值、caller 值,可以看到 x 值被更新、value 值为设置的以太币数量、caller 值为编辑器地址
-
部署 Demo1 合约
-
传入 Demo2 合约的地址、新 x 值,设置以太币数量,调用 Demo1 合约的 setDemo2X 方法;查看 Demo2 合约的 x 值、value 值、caller 值,可以看到 x 值被更新、value 值为设置的以太币数量、caller 值为 Demo1 合约地址
Interface
接口(Interface)用于定义合约之间的交互标准,确保不同合约之间可以互操作。
- 接口不能定义状态变量
- 接口只声明函数的签名,而不包含函数的实现;所有函数必须声明为
external
;不能包含构造函数 - 接口可以继承其他接口,但不能继承合约。
现有如下合约交互:
solidity
contract Counter {
uint public count;
function increment() public {
count += 1;
}
}
contract MyContract {
// 通过地址调用 Counter 合约的 increment 方法
function incrementCounter(address _counter) public {
Counter(_counter).increment();
}
// 通过地址获取 Counter 合约的状态变量 count
function getCount(address _counter) public view returns (uint) {
return Counter(_counter).count();
}
}
使用接口:
solidity
interface ICounter {
function increment() external;
function count() external view returns (uint);
}
contract MyContract {
function incrementCounter(address _counter) public {
ICounter(_counter).increment();
}
function getCount(address _counter) public view returns (uint) {
return ICounter(_counter).count();
}
}
通过 call 方法调用其他合约的方法
call 是一个比较底层的方法,可以用来调用其他合约的函数 同时发送以太。
solidity
contract TestCall {
event Log(string _str, uint _num, uint _value, address _sender);
function foo(
string calldata _str,
uint _num
) external payable returns (string memory, uint) {
emit Log(_str, _num, msg.value ,msg.sender);
return (_str, _num);
}
}
contract Call {
bytes public data;
function testCall(address _addr) public payable {
(bool success, bytes memory _data) = _addr.call
{
// 传输的以太数量; 若设置的以太数量小于该下限, 会报错
value: 100,
// gas 上限; 若消耗的 gas 大于该上限, 会报错
gas: 500000
}
(
// 传入 encodeWithSignature 包装后的调用数据; 第 1 参数是方法签名, 不能有空格, 不能用简写
abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
);
require(success, "call failed");
data = _data; // 返回值 _data 是被调用合约的方法的返回值
}
}
-
部署 TestCall 合约
-
传入字符串和数字,设置以太币数量,调用 TestCall 合约的 foo 方法;查看 Log 事件,可以看到传入的字符串、数字、以太币数量、调用者地址 (为编辑器地址)
-
部署 Call 合约
-
传入 TestCall 合约的地址,设置以太币数量,调用 Call 合约的 testCall 方法;查看 Call 合约的 data 值,可以看到 TestCall 合约的 foo 方法的返回值 (为 bytes 形式);查看 Log 事件,可以看到传入的字符串、数字、以太币数量、调用者地址 (为 Call 合约地址)