版本标识指令。用来启动某些编译器检查。比如pragma solidity ^0.8.0;表示当前合约只可以被主版本是0.8的编译器进行编译。又比如pragma solidity >= 0.7.0 < 0.9.0;表示当前合约可以被主版本大于等于0.7,小于0.9的编译器进行编译。
布尔类型。取值是true或者false
表示无符号或者有符号的整数类型。uint8uint256以及int8int256。以8位步长递增。uint、int分别代表的是uinit256以及int256。下面的案例就是越界了。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract OverFlow{
function add1() public pure returns (uint8){
uint8 x = 127;
uint8 y = x * 2;
return y;
}
function add2() public pure returns (uint8){
uint8 x = 240;
uint8 y = 16;
uint8 z = x + y;
}
}地址类型来表示一个账号,地址类型有两种。address:一个20个字节的值。address payable:表示可支付地址,与address相同也是20字节。不过有其自身的成员函数transfer和send。transfer执行失败会出异常,但是send执行失败会返回false,不会出异常。
<address>.balance(uint256) 以Wei为单位的地址类型的余额
<address payable>.transfer(uint256 amount) 向地址类型address发送amount数量的Wei,失败时抛出异常。
<address payable>.send(uint256 amount) returns (bool) 向地址address发送amount数量的Wei,失败时返回false。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract TestAddress{
function testTransfer(address payable x) public{
//任何一个合约都可以显式的转换成address类型
address myaddress = address(this);
if(myaddress.balance >= 10){
//如果x是一个合约地址,那么当transfer发生时,合约的receive或者fallback函数会随着transfer一起调用
x.transfer(10);
}
}
}如下代码所示,每new一次合约,便会创建一个新的合约。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract A{
function callMethod() public returns (uint){
//这里发生了什么?为什么会获取不到结果?
//每调用一次new,那么链上肯定会生成一个新的合约,在internal txns中可以看到
B b = new B();
return b.say();
}
}
contract B{
function say() public pure returns (uint){
return 10;
}
}如果希望调用B合约的say方法,可以使用如下方式来进行:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract A{
function callMethod(B b) public returns (uint){
return b.say();
}
}
contract B{
function say() public pure returns (uint){
return 10;
}
}地址类型还有几个比较偏向于底层的成员变量,类似于java的反射。这部分没有理解,后续需要进一步学习。
call
Delegatecall
Staticcall
下面这个案例其实就是transfer和call之间的区别。
transfer在进行转账时,可能会失败,因为gas问题
而使用call没有这个问题
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Address1{
uint total;
event logdata(uint x);
receive() external payable{
emit logdata(msg.value);
total += msg.value;
}
function getBalance() public view returns (uint){
return address(this).balance;
}
}
contract Call1{
//address owner;
constructor() payable{
//owner = msg.sender;
//这行代码可以在部署时,由外部合约控制账号给合约初始化一定的ether
msg.value;
}
//至于如何在合约中存储合约控制者的外部账号,暂不清楚
// function getOwner() public returns (address){
// return owner;
// }
//这个方法再当前合约给其他地址进行转账时,会失败,因为该方法的固定的gas设置为2300
//而调用transfer方法时,接收地址会调用receive或者fallback方法等,这些也需要gas
function transferEther(address toAddress) public returns (bool){
payable(toAddress).transfer(0.01 ether);
return true;
}
function transferEtherViaCall(address toAddress) public{
(bool success, ) = toAddress.call{value: 0.01 ether}("");
require(success, "transfer ether failed");
}
// function transfertoContract() payable public{
// payable(address(this)).transfer(10 ether);
// }
}call和delegatecall之间的区别
call会切换上下文,但是delegatecall不会
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
//合约被调用者
contract Called{
uint public number;
address public sender;
function setN(uint n) public{
number = n;
sender = msg.sender;
}
event logdata(uint x);
receive() external payable{
emit logdata(msg.value);
}
fallback() external{}
function getBalance() public view returns (uint){
return address(this).balance;
}
}
//合约调用者 注意:caller和called合约调用者和被调用者的变量名称应该完全一致
contract Caller{
uint public number;
address public sender;
function delegateCallN(address e, uint n) public{
bytes memory methodData = abi.encodeWithSignature("setN(uint256)", n);
e.delegatecall(methodData);
}
function callN(address e, uint n) public{
bytes memory methodData = abi.encodeWithSignature("setN(uint256)", n);
//不晓得为啥加上gas设置之后,就没法调用了;原因是gas不足,无法调用
e.call{gas:300000}(methodData);
//e.call(methodData);
}
}Caller是合约的调用者;Called是合约的被调用者
在Caller中我们定义了两个方法,一个是call方法,一个是delegatecall方法。
如果函数的运行导致变量值发生修改,那么改变的也是最终被调用者Called身上。
如果函数的运行导致变量值发生修改,那么改变的是Caller合约调用者里面的变量。
值类型在赋值存储时总是完整存储完整的拷贝。而对于一些复杂的数据类型占用的空间会比较大,开销会很大。因此就可以使用引用的方式。通过不同的名称的变量指向同一个值。引用类型有数组、结构体、映射。
引用类型都有一个额外的属性来标识数据的存储位置(使用引用类型必须明确地提供存储该类型的数据位置):
memory:内存。生命周期只存在于函数调用期间(生存期位于function()函数内,超过作用域失效)。
storage:存储。状态变量保存的位置。开销最大(生存期同contract一致,状态变量强制为storage)。
calldata:调用数据。用于函数参数不可变存储区域(存储在内存中,不上链。但是和memory不同的是calldata变量不可以修改,类似于final、immutable等,一般用于函数的参数,calldata一般更多用在参数上面,可以节省gas费用。如果使用memory类型,那么需要再将引用类型完整的复制一遍,而calldata不需要)。
function callData1(uint[] calldata _x) public pure returns (uint[] calldata) {
//calldata类型的变量不可以被修改
//calldata arrays are read-only
//_x[0] = 1;
return _x;
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract StorageTest {
//状态变量的存储位置是storage
string public text;
//参数的变量一般设置为calldata类型;external表示是对于智能合约外部是可见的,但是内部无法调用
function set(string calldata _text) external{
text = _text;
}
//view表示和智能合约交互只有查询操作;pure表示既不存储,也不修改
function get() external view returns (string memory){
return text;
}
}不同存储类型的数据在进行相互赋值时,有时候会产生独立的副本(也就是修改新变量的值不会影响原先变量的值),有时候会产生相互引用(修改新变量会影响原先变量的值)
合约的状态变量(storage)赋值给本地函数里的storage时,会创建引用,改变新变量会影响原先变量。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract DataStorage {
uint[] x = [1,2,3];
function callData1(uint[] calldata _x) public pure returns (uint[] calldata) {
//calldata类型的变量不可以被修改
//calldata arrays are read-only
//_x[0] = 1;
return _x;
}
function storage1() public{
uint[] storage xs = x;
xs[0] = 100;
}
}合约的状态变量(storage)赋值给memory,会创建独立的副本,修改其中一个不会对另外一个产生影响。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract DataStorage {
uint[] x = [1,2,3];
function callData1(uint[] calldata _x) public pure returns (uint[] calldata) {
//calldata类型的变量不可以被修改
//calldata arrays are read-only
//_x[0] = 1;
return _x;
}
function storage1() public{
uint[] storage xs = x;
xs[0] = 100;
}
function memory1() public{
uint[] memory xm = x;
xm[0] = 100;
xm[1] = 200;
x[0] = 400;
}
}memory赋值给memory,会创建引用。改变新变量时会修改原变量的值。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract DataStorage {
uint[] x = [1,2,3];
function callData1(uint[] calldata _x) public pure returns (uint[] calldata) {
//calldata类型的变量不可以被修改
//calldata arrays are read-only
//_x[0] = 1;
return _x;
}
function storage1() public{
uint[] storage xs = x;
xs[0] = 100;
}
function memory1() public{
//storage赋值给memory
uint[] memory xm = x;
xm[0] = 100;
xm[1] = 200;
x[0] = 400;
}
function memory2() public{
//memory类型的变量赋值给memory,会创建引用,修改一个,另外一个也会随之修改
//创建memory的数组,需要使用如下方式
//但是目前依然有困惑:为什么不可以使用uint[] memory xm1 = [1,2,3,4]...
//原因在于声明数组时必须指定数组的长度。uint[4] memory xm1 = [1,2,3,4]
uint[] memory xm1 = new uint[](7);
xm1[0] = 1;
xm1[1] = 2;
xm1[2] = 3;
xm1[3] = 4;
xm1[4] = 4;
xm1[5] = 5;
xm1[6] = 6;
uint[] memory xm2 = xm1;
xm2[0] = 7;
}
}状态变量:数据会存储在链上,gas消耗比较大 。类似于其他编程语言的成员变量。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract DataStorage {
//状态变量
uint[] x = [1,2,3];
function storage1() public{
uint[] storage xs = x;
xs[0] = 100;
}
局部变量:仅在函数执行过程中有效。函数退出之后,变量无效。局部变量仅存储在内存中,不上链,gas消耗低。上图的函数内部。
全局变量:可以在全局内使用的变量。无需进行声明。
blockhash(uint blockNumber) returns (bytes32): 指定区块的区块哈希,仅可用于最新的 256 个区块且不包括当前区块,否则返回 0
block.basefee(uint): 当前区块的基础费用 (EIP-3198 and EIP-1559)
block.chainid(uint): 当前链 id
block.coinbase(address payable): 挖出当前区块的矿工地址
block.difficulty(uint): 当前区块难度
block.gaslimit(uint): 当前区块 gas 限额
block.number(uint): 当前区块号
block.timestamp(uint): 自unix epoch 起始当前区块以秒计的时间戳
gasleft() returns (uint256): 剩余的gas
msg.data(bytes calldata): 完整的calldata
msg.sender(address): 消息发送者(当前调用者)
msg.sig(bytes4): calldata的前4字节(也就是函数标识符)
msg.value(uint): 随消息发送的wei的数量
tx.gasprice(uint):交易的 gas 价格
tx.origin(address): 交易发起者(完全的调用链)
keccak256((bytes memory) returns (bytes32):计算 Keccak-256 哈希
针对keccak256,引入一个案例,顺便学习solidity中字符串的操作。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.7;
contract Cryptograph {
function keccakMethod(string memory name) public returns (bytes32){
bytes memory b1 = bytes(name);
bytes32 result = keccak256(b1);
return result;
}
}可以用来存储一组数据。可以分为定长数组以及不定长数组。
定长数组 :在声明时就指定数组的长度,T[k]这种格式来表示
不定长数组(动态数组):在声明时不指定数组的长度,T[]这种格式来表示
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Array1{
//定长数组
uint[8] arr1;
bytes1[6] arr2;
address[3] arr3;
//不定长数组
uint[] arr4;
bytes1[] arr5;
address[] arr6;
//bytes类型比较特殊,表示数组
bytes arr7;
}注:关于bytes,是一种特殊类型的数组。类似于byte[],但是bytes的gas费用更低。bytes和string都可以用来表示字符串,对任意长度的原始字节数据使用bytes,对于任意长度的字符串,比如使用Unicode编码的字符,使用string
成员
1.length:表示的是当前数组的长度
2.push():只有动态数组(不定长数组)和 bytes数组拥有该成员,在末尾添加0
3.push(x):只有动态数组(不定长数组)和bytes数组拥有该成员,在末尾添加x
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Array1{
//定长数组
uint[8] arr1;
bytes1[6] arr2;
address[3] arr3;
//不定长数组
uint[] arr4;
bytes1[] arr5;
address[] arr6;
//bytes类型比较特殊,表示数组
bytes arr7;
function testArray() public {
//对于固定长度的数组会直接初始化数组,默认值全部为0;无法调用push方法
//arr1.push();
//只有不定长数组,也就是动态数组可以调用push方法;动态数组初始化时长度为0,无填充
arr4.push();
arr4.push(1);
arr4.push(2);
arr4.push(3);
arr4.push(4);
arr4.push(5);
}
} 4.pop():只有动态数组(不定长数组)和bytes数组拥有该成员,移除数组最后一个元素。思考:为什么只提供移除数组最后一个元素的方法,而没有提供移除其他元素的方法呢?
解决了一个数组越界提示非常模糊的bug。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Array4{
uint[] public numbers;
/**
* 删除数组中间某个元素时,不可以像其他编程语言一样,删除该元素,其他后面的元素往前移动
* 这样会消耗巨量的gas
* 删除数组两种方式:
* 1.像上述提及的一样,比如[1,2,3,4,5,6] -----remove(2)下标-----[1,2,4,5,6,6]---pop---[1,2,4,5,6]
* 2.还有另外一种方式:比如[1,2,3,4,5,6] -----remove(2)下标,最后一个元素赋值给需要删除的元素 [1,2,6,4,5,6]--pop---[1,2,6,4,5]
* 下面这种方式更加节省gas费,但是如果需要保障数组的顺序,那么只能使用方式1
*/
function remove1(uint index) public{
uint length = numbers.length;
if(index == length - 1){
numbers.pop();
}else {
numbers[index] = numbers[length - 1];
numbers.pop();
}
}
/**
* [1,2,3,4] remove index 2
*/
function remove2(uint index) public{
uint length = numbers.length;
if(index == length - 1){
numbers.pop();
}else {
for (uint i = index; i < length - 1; i++) {
numbers[i] = numbers[i + 1];
}
numbers.pop();
}
}
function test1() external{
numbers = [1,2,3,4,5];
remove1(2);
// [1,2,4,5]
assert(numbers.length == 4);
assert(numbers[0] == 1);
assert(numbers[1] == 2);
assert(numbers[2] == 5);
assert(numbers[3] == 4);
}
function test2() external{
numbers = [1,2,3,4,5];
remove2(2);
// [1,2,4,5]
// assert(numbers.length == 4);
// assert(numbers[0] == 1);
// assert(numbers[1] == 2);
// assert(numbers[2] == 5);
// assert(numbers[3] == 4);
}
}创建数组的规则
1.对于memory修饰的不定长数组,可以使用new操作符来创建,但是创建之后长度不能改变;对于memory修饰的固定长度数组,直接赋值即可。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Array2{
//数组的创建规则
function memoryArray() public{
//创建内存数组的两种形式,但是目前遇到了下图的疑问,暂未解决
uint[] memory arr1 = new uint[](3);
arr1[0] = 1;
arr1[1] = 3;
arr1[2] = 5;
uint[4] memory arr2 = [uint(1),5,7,9];
string[2] memory arr3 = ["hello", "solidity"];
//uint a = 1;
}
} 2.数组字面常数,是用中括号来初始化数组的一种方式。数组里面的每一个元素的值是以第一个元素的值为基准的,比如[1,2,3],该数组里面的所有的元素都会被认为是uint8类型的,如果使用uint[]来接收(uint默认256),就会出现错误。解决办法也很简单,[uint(1),2,3]这样就解决了问题。这也是上述案例写法的原因。
3.创建的如果是不定长数组,需要一个一个进行赋值。和其他编程语言一致。
用 struct关键字定义一个就结构体,和类比较相似 。
结构体赋值见下面几种方式
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract StructTest{
struct Car {
string name;
uint year;
address owner;
}
//定义一个结构体的状态变量
Car public car;
Car[] public cars;
function examples() external{
//声明赋值结构体的几种方式
Car memory toyota = Car("Toyota", 2000, msg.sender);
Car memory bmw = Car({name: "BMW", year: 2022, owner: msg.sender});
Car memory tesla;
tesla.name = "Tesla";
tesla.year = 2021;
tesla.owner = msg.sender;
//将结构体装入到数组中
cars.push(toyota);
cars.push(bmw);
cars.push(tesla);
//再单独添加一个新的结构体数据
cars.push(Car({name: "ferrari", owner: msg.sender, year: 2009}));
//状态变量赋值给本地memory变量,不会影响状态变量的值
Car memory _car = cars[0];
_car.name = "Benz";
_car.year = 1999;
//原本是bmw,修改之后应该是马自达
Car storage _car2 = cars[1];
_car2.name = "mzd";
_car2.year = 2022;
}
}在上述代码中 ,我们修改了cars[0],但是状态变量中是没有发生修改的;但是我们修改cars[1],状态变量是会发生修改的。
通过key去查询value值。对于学过编程语言的同学来说,应该理解不难。
映射的规则:
- 映射的key值只可以是solidity规定的默认数据类型。不可以设置结构体。
- 映射的存储位置必须设置为storage,映射可以作为合约的状态变量,函数内的storage变量。不能用作public函数的参数或者返回值。
- 映射不存储key的信息,也不会有length的信息。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.7;
contract MappingTest {
mapping (uint => address) public numbers;
mapping (uint => uint) public intPairs;
mapping (address => uint) public balances;
mapping (address => mapping (address => bool)) public isFriends;
function examples() external {
//给映射进行赋值操作
balances[msg.sender] = 123;
uint balance1 = balances[msg.sender];
//Explicit conversions to and from address are allowed for uint160, integer literals, bytes20 and contract types.
uint balance2 = balances[address(1)];
balances[msg.sender] += 123;
//delete操作也不是真正的删除,而是重置
delete balances[msg.sender];
isFriends[msg.sender][address(this)] = true;
//映射必须是storage类型
mapping (address => bool) storage friendsInfo = isFriends[msg.sender];
// 返回的结果是true
bool result = friendsInfo[address(this)];
//返回结果是false
bool result2 = friendsInfo[address(1)];
//testMap(balances);
}
//Data location must be "memory" or "calldata" for parameter in function, but none was given.
//但是mapping不可以设置memory或者calldata
// function testMap(mapping (address => uint) bals) public{
// uint bal = bals[msg.sender];
// }
}自行实现可迭代映射
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract IterableMapping {
//存储账号和金额的映射
mapping (address => uint) balances;
//存储账号是否存储过
mapping (address => bool) inserted;
//存储所有的key值
address[] keys;
function deposit(uint _value) external{
setBalance(msg.sender, _value);
}
function setBalance(address _key, uint _value) public{
balances[_key] = _value;
if(!inserted[_key]){
inserted[_key] = true;
keys.push(_key);
}
}
function length() external view returns (uint){
return keys.length;
}
function first() external view returns (uint){
return balances[keys[0]];
}
function last() external view returns (uint){
return balances[keys[keys.length - 1]];
}
function get(uint _index) external view returns (uint){
require(_index <= keys.length - 1, "index out of bounds");
return balances[keys[_index]];
}
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract EnumTest {
enum Status{
None,
Pending,
Shipped,
Completed,
Rejected,
Canceled
}
Status public sta;
function get() view external returns (Status){
return sta;
}
function set(Status _sta) external{
sta = _sta;
}
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract TodoList {
struct Todo {
string text;
bool isCompleted;
}
Todo[] public todos;
function create(string calldata _text) external {
todos.push(Todo({
text: _text,
isCompleted: false
}));
}
function updateText(uint _index, string calldata _text) external {
todos[_index].text = _text;
//下面还有一种方式,适合需要修改的变量属性非常多的时候
//将索引_index下标的数据取出来,放到storage位置,然后再去修改
Todo storage todo = todos[_index];
todo.text = _text;
}
function get(uint _index) external view returns (string memory, bool){
Todo memory todo = todos[_index];
return (todo.text, todo.isCompleted);
}
function toggleCompleted(uint _index) external{
todos[_index].isCompleted = !todos[_index].isCompleted;
}
}事件是EVM中日志的抽象。具有两个特征:
1.响应:应用程序可以通过订阅、监听这些事件,并在前端做出响应。
2.经济:事件是EVM中比较经济的存储方式。每个gas消耗2000,而存储一个新变量至少需要20000gas。
事件的声明:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract EventTest {
//indexed标记可以理解为索引,在以太坊中作为一个topic来进行存储,方便筛查转账地址和接收地址的转账事件
//每个event事件中最多有3个标记为indexed的变量。每个indexed变量的大小固定是256bit。
//事件的hash、这三个indexed变量一般在EVM中被存储为topic。
event transfer(address indexed _from, address indexed _to, uint val);
}如上述介绍,下图中出现的Topics[0]为此事件的keccak256哈希,Topics[1]为携带indexed变量的from,Topics[2]为携带indexed变量的to。Data也就是value值。
事件的调用:
如上述编写的事件,每当我们执行转账交易时,应该触发该事件。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract EventTest {
//indexed标记可以理解为索引,在以太坊中作为一个topic来进行存储,方便筛查转账地址和接收地址的转账事件
//每个event事件中最多有3个标记为indexed的变量。每个indexed变量的大小固定是256bit。
//事件的hash、这三个indexed变量一般在EVM中被存储为topic。
event transfer(address indexed _from, address indexed _to, uint val);
function f_transfer(address _to, uint _val) external {
//执行转账逻辑,调用该事件
emit transfer(msg.sender, _to, _val);
}
}声明了函数拥有的特性。主要的使用场景是函数运行前检查,比如检查地址等。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract EventTest {
address owner;
constructor(){
owner = msg.sender;
}
//indexed标记可以理解为索引,在以太坊中作为一个topic来进行存储,方便筛查转账地址和接收地址的转账事件
//每个event事件中最多有3个标记为indexed的变量。每个indexed变量的大小固定是256bit。
//事件的hash、这三个indexed变量一般在EVM中被存储为topic。
event transfer(address indexed _from, address indexed _to, uint val);
function f_transfer(address _to, uint _val) external {
//执行转账逻辑,调用该事件
emit transfer(msg.sender, _to, _val);
}
modifier ownerPermission {
require(msg.sender == owner);
}
//表示的含义是仅当是合约的拥有者才有权限去修改
function changeOwner(address _newOwner) external ownerPermission{
owner = _newOwner;
}
}virtual如果父合约中的方法希望子合约来进行重写,则需要加上该关键字。
override子合约重写了父合约中的函数,需要加上该关键字。
在下面的B合约中,存在3个函数。其中m1、m2会返回b;m3会返回a。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract A {
function m1() public virtual pure returns (string memory){
return "a";
}
function m2() public virtual pure returns (string memory){
return "a";
}
function m3() public pure returns (string memory){
return "a";
}
}
contract B is A{
//如果B里面没有一个方法,部署之后会有A的几个方法
function m1() public override pure returns (string memory){
return "b";
}
function m2() public override pure returns (string memory){
return "b";
}
}合约可以继承多个合约。但是需要按照辈分从高到低来编写(从基准到派生的顺序)。如果一个同名函数在多个合约中同时存在,那么子合约在继承时,必须重写,否则报错。重写在多个父合约中都存在的函数时,override关键字后面需要加上所有的父合约名称。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract X {
function m1() public virtual pure returns (string memory) {
return "xm1";
}
function m2() public virtual pure returns (string memory){
return "xm2";
}
function x() public pure returns (string memory){
return "x";
}
}
contract Y is X{
function m1() public virtual override pure returns (string memory) {
return "ym1";
}
function m2() public virtual override pure returns (string memory){
return "ym2";
}
function y() public pure returns (string memory){
return "y";
}
}
contract Z is X, Y{
//m1 m2在两个合约中都有,所以必须要重写实现
//m1 m2在X和Y合约中都有 ,所以override后面应该携带所有包含该函数的父合约,顺序无要求
function m1() public override (Y,X) pure returns (string memory){
return "zm1";
}
function m2() public override (X,Y) pure returns (string memory){
return "zm2";
}
}如果父合约构造函数没有参数,那么子合约在继承时无特殊注意事项。但是如果父合约有构造函数,那么子合约在继承时需要指定所有的参数。
1.一种是直接在继承列表中调用父合约的构造函数is Base(8)
2.作为子合约构造函数定义头的一部分。Base(_y * _y)这里面主要的目的是给父合约指定参数的值,至于里面的_y * _y不重要 ,换成其他的也是可以的,仅仅是进行了一次运算,再赋值。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Base {
uint public x;
constructor(uint _x){
x = _x;
}
}
contract Derived is Base{
uint public y;
constructor(uint _y) Base (_y * _y){
y = _y;
}
}
contract Derived2 is Base(8){
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract X {
function m1() public virtual pure returns (string memory) {
return "xm1";
}
function m2() public virtual pure returns (string memory){
return "xm2";
}
function x() public pure returns (string memory){
return "x";
}
}
contract Y is X{
function m1() public virtual override pure returns (string memory) {
return "ym1";
}
function m2() public virtual override pure returns (string memory){
return "ym2";
}
function y() public pure returns (string memory){
return "y";
}
}
contract Z is X, Y{
//m1 m2在两个合约中都有,所以必须要重写实现
function m1() public override (Y,X) pure returns (string memory){
return "zm1";
}
function m2() public override (X,Y) pure returns (string memory){
return "zm2";
}
}
contract M is X, Y {
function m1() public override (Y,X) pure returns (string memory){
return "mm1";
}
function m2() public override (X,Y) pure returns (string memory){
return "mm2";
}
function callParent() public pure returns (string memory){
//调用super.xxx方法,如果多个父合约都有,那么按照从右往左的顺序来
return super.m1();
}
function callParent2() public pure returns (string memory){
//调用super.xxx方法,如果多个父合约都有,那么按照从右往左的顺序来
//也可以直接使用如下语法来进行调用
return X.m1();
}
}重点关注Z合约的foo函数,里面调用了super.foo(),最终return接收到的值是Y合约的值,但是通过日志打印可以发现全部合约都有被调用。返回值遵循从左往右调用的原则,但是所有父合约的方法都有被调用。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract W {
event Log(string message);
function foo() virtual public returns (string memory){
emit Log("W.foo");
return "wf";
}
}
contract X is W{
function foo() virtual override public returns (string memory){
emit Log("X.foo");
super.foo();
return "xf";
}
}
contract Y is W{
function foo() virtual override public returns (string memory){
emit Log("Y.foo");
super.foo();
return "yf";
}
}
contract Z is X, Y{
string public text;
function foo() override(X,Y) public returns (string memory){
emit Log("Z.foo");
text = super.foo();
return text;
//return "zf";
}
}private:仅合约内部可见。
internal:仅合约内部和子合约可见。
public:合约内外均可见。
external:仅合约外部可见。
向合约发送ether,也就是合约接收ether的三种方式:
- 部署转账
- 通过执行函数方法转账,方法需要设置
payable - 直接转账,需要实现
fallback或者receive,同时设置payable
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Payable {
//标注了payable,就表示其可以接收以太坊主币
function deposit() external payable{
}
function getBalance() external view returns (uint){
return address(this).balance;
}
}上述deposit函数标记了payable属性,那么调用该方法时便可以传递ether主币。
但是如果希望部署之后的合约可以直接接收ether主币,则需要实现如下功能。
fallback函数在以下场景会被触发:
1.当执行不存在的方法时
2.直接发送ether主币时
在solidity 0.8版本时出现了一个新的函数,receive函数适用于只接收ether主币的情况。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract FallBack {
//给合约发送eth还有另外一种方式,可以将构造函数设置为payable,在部署合约时给合约传递
constructor() payable{}
event Log(string func, address sender, uint value, bytes data);
fallback() external payable{
emit Log("fallback", msg.sender, msg.value, msg.data);
}
receive() external payable{
emit Log("receive", msg.sender, msg.value, "");
}
}发送Ether的三种方式:
- transfer方法:转发固定的2300 gas,如果失败则回退
- send方法:转发固定的2300 gas,返回bool
- call方法 :转发剩余所有的gas,返回bool和data
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract SendEther {
//部署的合约的时候,直接给合约传入主币
constructor() payable{}
//可以直接接收其他地址发送主币
receive() external payable{}
function transferEther(address payable _to) external payable {
//发送2000wei,此处发送只会携带2300 gas;如果gas耗尽或者其他异常则会revert报出异常
_to.transfer(2000);
}
function sendEther(address payable _to) external payable{
//如果发送失败,不会revert,而是返回一个bool类型的值
bool result = _to.send(2000);
require(result, "send failed");
}
function callEther(address payable _to) external payable{
//call方法执行返回两个值,一个bool类型的值,一个是比如调用智能合约等返回的其他类型的数据
(bool result, bytes memory data) = _to.call{value:2000}("");
require(result, "call failed");
}
}
//另外创建一个合约用来接收ether,接收到ether之后会调用receive函数
contract EtherReceive{
event Log(uint amount, uint gas);
receive() external payable{
emit Log(msg.value, gasleft());
}
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract EtherWallet{
address payable owner;
constructor(){
owner = payable(msg.sender);
}
receive() external payable{}
function withdraw(uint _amount) external{
require(msg.sender == owner, "caller is not owner");
payable(msg.sender).transfer(_amount);
}
function getBalance() external view returns (uint){
return address(this).balance;
}
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract A {
function setX(address _add, uint _x)external{
//另一个合约当做类型,传递地址
B(_add).setX(_x);
}
function setX2(B _add, uint _x) external{
//还有另外一种调用方式,直接以B合约类型作为参数
_add.setX(_x);
}
function getX(address _add) external view returns (uint){
return B(_add).getX();
}
//返回值也可以使用下面这种方式来编写
function getX2(address _add) external view returns (uint result){
result = B(_add).getX();
}
function setXAndReceiveEther(address _add, uint _x) external payable{
B(_add).setXAndReceiveEther{value:msg.value}(_x);
}
function getXAndBal(address _add) external view returns (uint x, uint y){
(x, y) = B(_add).getXAndEther();
}
}
contract B{
uint public x;
uint public bal;
function setX(uint _x)external {
x = _x;
}
function getX() external view returns (uint){
return x;
}
function setXAndReceiveEther(uint _x) external payable{
x = _x;
bal = msg.value;
}
function getXAndEther() external view returns (uint, uint){
return (x, bal);
}
}如果一个智能合约中有没有实现的方法,那么合约必须要标注为abstract,不然编译器会报错。
abstract contract XXX{
function doSomething() external pure returns (uint);
}接口类似于抽象合约,但是它不实现任何功能。接口的要求如下:
- 不能包含状态变量
- 不能包含构造函数
- 不能继承除接口外的其他合约
- 所有函数都必须是external且不能有函数体
- 继承接口的合约必须实现接口定义的所有功能
接口制定了一个统一的标准规范。约定了Dapp如何和合约进行交互。比如ERC20或者ERC721
ERC721接口:
interface IERC721 is IERC165 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function setApprovalForAll(address operator, bool _approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
interface ICounter {
function count() external view returns (uint);
function increase() external;
}
//合约部署时,不是CounterImpl和CallInterface即可,接口无法进行部署
contract CounterImpl{
uint public number;
function count() external view returns (uint){
return number;
}
//该方法和接口中的方法同名,因为调用时需要保证方法名称相同
function increase() external{
number ++;
}
//当前方法再接口中没有,不过也不会影响最终合约的正常运行
function decrease() external{
number --;
}
}
contract CallInterface {
function call1(address _address) external{
ICounter(_address).increase();
ICounter(_address).count();
}
}solidity提供了三种方式抛出异常的方式:error、require、assert
error
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
error insufficientBalance(uint available, uint required);
contract ErrorException {
function transfer(address _to, uint amount) external payable{
if(amount > msg.value){
revert insufficientBalance(msg.value, amount);
}
}
}require
语法格式为:require(检查条件,"异常的描述"),可以直接在合约函数中直接调用,使用起来比较简单,但是缺点就是gas相较于error要高。
function transfer(address _address, uint amout) external payable{
require(msg.value > amount, "balance not enough");
.....
}assert
和require比较类似,但是它不可以解释出错的原因,用法assert(检查条件)
推荐使用error
在前面的案例中,我们介绍过使用call来发送ETH主币。今天我们将利用Call来调用另一个合约。
call 是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data),分别对应call是否成功以及目标函数的返回值。
call是官方推荐的发送ETH的方法。
不推荐使用call来调用另一个合约。
call的使用方式如下:
目标合约地址.call((abi.encodeWithSignature(...)))
其中二进制编码由上述括号内的代码来实现。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
error executeFailed(string _message);
contract A {
uint public x;
string public message;
event Log(string _message);
receive() external payable{}
// fallback() external payable {
// emit Log("fallback executed");
// }
function m1(string memory _message, uint _x) external payable returns (bool, uint){
x = _x;
message = _message;
return (true, 10086);
}
}
contract B {
bytes public data;
function call1(address _addr) external payable{
//特别注意,此处uint必须要写成uint256类型
//abi二进制编码的时候千万不要多了一个空格
(bool result, bytes memory _data) = _addr.call{value:1234,gas:100000}(abi.encodeWithSignature("m1(string,uint256)", "hello call", 1234));
if(!result){
revert executeFailed("call failed");
}
// require(result, "call failed");
data = _data;
}
function callNonExist(address _addr) external {
(bool result, ) = _addr.call(abi.encodeWithSignature("nonExist()"));
if(!result){
revert executeFailed("call none exist function");
}
require(result, "call non-exists function");
}
}和call比较类似。delegatecall表示委托调用的意思。下图表示的是call和delegatecall之间的区别:
用户A通过call合约B再call合约C的时候,执行的是合约C的函数。语境也是C的:msg.sender是B,如果函数改变了一些状态变量,产生的效果也会作用于C的变量上。
用户A通过call合约B再delegatecall合约C的时候,执行的是C的合约的函数,但是语境依然是B的:msg.sender是A的地址,如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。
使用场景:暂不理解
- 代理合约(
Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。 - EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合同的代理合同。 更多信息请查看:钻石标准简介。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
//合约调用发起者
contract B {
uint public num;
address public sender;
//通过发起call调用
function callSetVariable(address _addr, uint _num) external payable{
(bool result, bytes memory data) = _addr.call(abi.encodeWithSignature("setVariable(uint256)", _num));
}
//发起delegatecall
function delegateCallSetVariable(address _addr, uint _num) external payable{
(bool result, bytes memory data) = _addr.delegatecall{gas:100000}(abi.encodeWithSignature("setVariable(uint256)", _num));
}
}
//合约被调用者
contract C {
uint public num;
address public sender;
function setVariable(uint _num) public payable{
num = _num;
sender = msg.sender;
}
}调用 delegatecall:
EOA-----call B-----delegatecall C,变量的修改会发生在合约中。msg.sender=EOA
EOA-----call B------call C
变量的修改会发生在合约C中。msg.sender也会是B
delegatecall的主要使用场景是用在合约升级中。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
//利用ProxyContract来调用那两个Update合约,这样可以实现合约的升级
//变量的值依然存储在ProxyConntract中
contract ProxyContract {
uint public num;
address public sender;
uint public value;
function setNum(address _target, uint _num) external payable{
(bool result, bytes memory data) = _target.delegatecall(abi.encodeWithSignature("setNum(uint256)", _num));
require(result, "delegatecall failed");
}
}
//原先的合约逻辑
contract UpdateContract{
uint public num;
address public sender;
uint public value;
function setNum(uint _num) external payable{
num = _num + 2;
sender = msg.sender;
value = msg.value;
}
}
//升级之后的合约逻辑
contract UpdateContract2{
uint public num;
address public sender;
uint public value;
function setNum(uint _num) external payable{
num = _num * 2;
sender = msg.sender;
value = msg.value;
}
}去中心化交易所uniswap便是利用工厂合约factory创建了无数个币对合约。
create语法创建合约比较简单,就是通过new关键字来创建一个新的合约。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Account {
address public owner;
address public factory;
constructor(address _owner) payable{
factory = msg.sender;
owner = _owner;
}
}
contract AccountFactory{
Account[] public accounts;
function createAccount(address _owner) external payable{
Account account = new Account{value:12345}(_owner);
accounts.push(account);
}
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Pair {
address public factory;
address public token1;
address public token2;
constructor() {
factory = msg.sender;
}
function initialize(address _token1, address _token2) external {
require(msg.sender == factory, "failed");
token1 = _token1;
token2 = _token2;
}
}
contract PairFactory{
mapping (address => mapping (address => address)) public tokenPair;
address[] public allPairs;
//创建币对合约
function createContract(address _token1, address _token2)external {
Pair pair = new Pair();
pair.initialize(_token1, _token2);
address pairAddress = address(pair);
allPairs.push(pairAddress);
tokenPair[_token1][_token2] = pairAddress;
tokenPair[_token2][_token1] = pairAddress;
}
}create2可以使得智能合约在部署之前便可以得到合约的地址。
create方式生成合约地址的方式是
hash(创建者地址, nonce)
虽然创建者地址是不变的,但是nonce的值却会随着时间而改变。因此使用create无法预测合约的地址。
create2可以通过提前计算出合约的地址。计算的数据主要由4部分组成:
0xFF:一个常数- 创建者地址
- salt:加盐处理。
- 待部署合约的字节码(
bytecode)
create2的使用方式和create非常类似,只是需要传递salt。如果构造函数有参数,则传递入params参数,如果构造函数是payable,则可以在创建合约时传递_value数值的ETH主币。
Contract contract = new Contract{salt : _salt, value : _value}(params);创建出来的合约地址和验证的合约地址是一致的。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract Pair {
address public factory;
address public token1;
address public token2;
constructor() {
factory = msg.sender;
}
function initialize(address _token1, address _token2) external {
token1 = _token1;
token2 = _token2;
}
}
contract PairFactory2{
mapping (address => mapping (address => address)) public tokenPair;
//保存所有的合约地址信息
address[] public tokenPairs;
function createPair2(address _token1, address _token2) external returns (address pairAddress){
require(_token1 != _token2, "could not create same token pair");
//这一步为了保证输入不同的地址对得到的顺序是一致的,后面要进行哈希运算
(address token1, address token2) = _token1 < _token2 ? (_token1, _token2) : (_token2, _token1);
bytes32 salt = keccak256(abi.encodePacked(token1, token2));
//使用create2创建新的合约 salt是bytes32位的
Pair pair = new Pair{salt : salt}();
pair.initialize(token1, token2);
pairAddress = address(pair);
tokenPairs.push(pairAddress);
tokenPair[token1][token2] = pairAddress;
tokenPair[token2][token1] = pairAddress;
}
function calculateAddress(address _token1, address _token2) public view returns (address predicatedAddress){
require(_token1 != _token2, "could not create same token pair");
//这一步为了保证输入不同的地址对得到的顺序是一致的,后面要进行哈希运算
(address token1, address token2) = _token1 < _token2 ? (_token1, _token2) : (_token2, _token1);
bytes32 salt = keccak256(abi.encodePacked(token1, token2));
//需要四个参数:0xff、当前创建者合约地址、盐、bytecode
bytes32 hashcode = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(type(Pair).creationCode)));
predicatedAddress = address(uint160(uint(hashcode)));
}
}删除合约之后,再与合约交互时,不会有任何响应。
删除合约,会将合约内的主币强制转给当前合约的调用者。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract KillSelf{
constructor() payable{}
function kill() external {
selfdestruct(payable(msg.sender));
}
}
contract HelperContract{
function destroy(KillSelf _kill) external{
_kill.kill();
}
function getBalance() external view returns (uint){
return address(this).balance;
}
}库合约就是类似其他编程语言中写的工具类、类库、jar包等等。
库合约的特征如下:
- 不能存在除了constant之外的状态变量
- 不能够继承或被继承
- 不能接收以太币
- 不可以被销毁
库合约的使用方式如下代码所示:两种
1.using xxx for xxx
2.库合约.xxx方法
library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
bytes array;
function toString(uint256 value) public pure returns (string memory) {
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
function toHexString(uint256 value) public pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
}
return toHexString(value, length);
}
function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
}
contract A {
//库合约使用方式一
using Strings for uint256;
function getString1(uint256 _number) external pure returns (string memory){
return _number.toHexString();
}
//库合约使用方式二:
function getString2(uint256 _number) external pure returns (string memory){
return Strings.toHexString(_number);
}
}哈希运算的特征:
单向性:得到某个数的哈希值是一个非常简单的过程,但是反过来却非常难。
雪崩性:数值的细微改变都会引起哈希值发生较大的变化。
一致性:同一个数值经过同一个哈希运算,无论执行多少次结果都是一致的。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract HashFunction {
//为什么这里的return bytes32不需要加memory,因为bytes32是定长数组;不定长数组需要添加
//默认基本类型,不需要刻意指定 存储类型;struct、动态数组、映射、string等引用类型必须指定存储类型,否则编译会报异常
function hash1(string memory _text, uint _num, address _address) external pure returns (bytes32) {
//使用hash运算用固定函数,计算之前将数据进行打包。可以有两种方式 abi.encodePacked abi.encode
return keccak256(abi.encodePacked(_text, _num, _address));
}
function hash2(string memory _text, uint _num, address _address) external pure returns (bytes32) {
//使用hash运算用固定函数,计算之前将数据进行打包。可以有两种方式 abi.encodePacked abi.encode
return keccak256(abi.encode(_text, _num, _address));
}
//“AAA”,"AAAA":bytes: 0x00000000000000000000000000000000000000000000000000000000000000400000000000000
//00000000000000000000000000000000000000000000000008000000000000000000000000000000000000
//00000000000000000000000000009e2809c414141e2809d00000000000000000000000000000000000000000
//0000000000000000000000000000000000000000000000000000000000000000000044141414100000000000
//000000000000000000000000000000000000000000000
function encode(string memory _text1, string memory _text2) external pure returns (bytes memory){
return abi.encode(_text1, _text2);
}
//bytes: 0xe2809c414141e2809d41414141
function encodePacked(string memory _text1, string memory _text2) external pure returns (bytes memory){
return abi.encodePacked(_text1, _text2);
}
}通过import指令可以将外部的合约引入到当前合约内部中来进行使用。
import {HashFunction as Hash1} from "./HashFunction.sol";将当前目录下某个合约文件导入到当前合约中来进行使用。{}内部是对合约名称取了一个别名,防止导入多个同名合约时混淆的问题。
import {Address} from'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';下面这种方式是导入一个网络中存在的合约到当前合约中。
因为导入的Address是一个library。所以使用方式有以下两种
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {HashFunction as Hash1} from "./HashFunction.sol";
import {Address} from'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
contract A {
Hash1 hash = new Hash1();
//因为Address这个合约是library,所以不可以直接new
using Address for address;
function importDemo1(string memory _text, uint _num, address _address) external view returns (bytes32){
return hash.hash1(_text, _num, _address);
}
function importDemo2(address _address) external view returns (bool){
return _address.isContract();
}
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {HashFunction as Hash1} from "./HashFunction.sol";
import {Address} from'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
contract A {
Hash1 hash = new Hash1();
function importDemo1(string memory _text, uint _num, address _address) external view returns (bytes32){
return hash.hash1(_text, _num, _address);
}
//library合约的另外一种使用方式
function importDemo2(address _address) external view returns (bool){
return Address.isContract(_address);
}
}ABI全称为Application Binary Interface.应用二进制接口,是和以太坊智能合约进行交互的标准。我们要将数据发送到合约,或者和合约进行交互,需要以一种合约可以识别的方式。也就是说,它们需要被编码。执行编码操作就需要使用到ABI。
在solidity中一共有4种方式可以用来进行编码:abi.encode()、abi.encodePacked()、abi.encodeWithSignature()、abi.encodeWithSelector()。
abi.encode()
会将每个参数填充为32个字节的数据,并且最终拼接在一起。如果需要和合约进行交互,那么需要使用该方法。
abi.encodePacked()
将参数按照所需最小空间进行编码。和上面比较类似,但是会将填充的很多0去掉。比如uint只会用一个字节来表示。当希望节省空间,同时不希望与合约进行交互的时候可以使用。比如进行哈希运算时。
abi.encodeWithSignature()
和abi.encode()比较类似,只不过会在abi.encode()编码结果前面加上4个字节的函数选择器(就是通过函数名称和函数参数进行签名来标识函数)。只不过第一个参数为函数签名。方法名(方法参数类型)。在调用其他合约的函数时使用。
abi.encodeWithSelector()
和abi.encodeWithSignature()的功能基本类似。只不过第一个参数为函数选择器。为函数签名keccak256哈希值的前4个字节。
可以将abi.encode编码的结果进行解码。还原成原本的数据。
下面这个案例讲解了encode和decode的使用。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract ABIEncodeAndDecode {
struct Car {
string name;
uint[] nums;
}
//abi.encode可以使用abi.decode来进行解码
function encode(uint _a, address _b, uint[] calldata _c, Car calldata _d) external pure returns (bytes memory){
return abi.encode(_a, _b, _c, _d);
}
//Furthermore, structs as well as nested arrays are not supported.不支持struct
function encodePacked(uint _a, address _b, uint[] calldata _c) external pure returns (bytes memory){
return abi.encodePacked(_a, _b, _c);
}
function decode(bytes calldata data) external pure returns (uint _a, address _b, uint[] memory _c, Car memory _d){
(_a, _b, _c, _d) = abi.decode(data, (uint, address, uint[], Car));
}
}// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract ABIEncode2 {
//abi.encode可以使用abi.decode来进行解码
function encode(uint _a, address _b, uint[] calldata _c) external pure returns (bytes memory){
return abi.encode(_a, _b, _c);
}
function encodeWithSignature(uint _a, address _b, uint[] calldata _c) external pure returns (bytes memory){
return abi.encodeWithSignature("encode(uint256,address,uint256[])", _a, _b, _c);
}
function encodeWithSelector(uint _a, address _b, uint[] calldata _c) external pure returns (bytes memory){
return abi.encodeWithSelector(bytes4(keccak256("encode(uint256,address,uint256[])")), _a, _b, _c);
}
}和java中的反射比较类似。找到方法的参数签名,利用传递进来的参数,进而调用相应的函数。
当我们调用智能合约时,我们实际上是向合约提交发送了一段数据。在remix中input可以完整的显示该内容。发送的数据中前4个字节是selector,也就是函数选择器。
msg.data是一个全局变量。其中里面的值是调用函数时传递的完整数据。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract SelectDemo {
event Log(bytes data);
//当我们调用m1方法时,传递过去的完整参数会保存在msg.data全局变量中
// 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// input:0x0c7dff020000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
// data:0x0c7dff020000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
function m1(address _address)external {
emit Log(msg.data);
}
}0x0c7dff020000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4这一串数据应该如何解析呢?
前四个字节是函数选择器:0c7dff02
后面32个字节是输入的参数:0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
而这个刚好就是我们传递进来的地址参数。
函数签名,其实就是用函数方法名称(逗号分割出来的参数类型)来表示。比如上述案例中的m1(address)就是函数签名。
method id呢?是被定义为函数签名的keccak哈希值的前四个字节。当selector和method id相同时,便会调用该函数。
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
contract SelectDemo {
event Log(bytes data);
//当我们调用m1方法时,传递过去的完整参数会保存在msg.data全局变量中
// 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// input:0x0c7dff020000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
// data:0x0c7dff020000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
// selector:0x0c7dff02
// address:0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
function m1(address _address)external {
emit Log(msg.data);
}
//验证一下返回值是否是上面的selector 0x0c7dff02
function m2() external pure returns (bytes4 sele){
sele = bytes4(keccak256("m1(address)"));
}
}





























