16.函数重载
16_01.重载
函数重载(
overloading
):即函数名字相同,但输入的参数类型不同的函数可以同时存在;(被视为是不同的函数)
Solidity
不允许修饰器modifier
重载;重载的函数经过编译之后,由于不同的参数类型,都变成了不同的函数选择器(
selector
,29节有介绍);
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract A{// 无传入参数,输出"No parameter"function saySomething() public pure returns (string memory) {return "No parameter";}// 传入string,输出stringfunction saySomething(string memory str) public pure returns (string memory){return str;}
}
16_02.实参匹配
调用重载函数时,会把输入的实际数据和函数参数的类型进行匹配,若出现多个匹配的重载函数,会报错;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract A{uint256 data = 0;// 传入参数是uint8function Add(uint8 num) public {data += num;}// 传入参数是uint256function Add(uint256 num) public {data += num;}function callAdd() public pure returns (string memory){// 50即可用是uint8,也可以是uint256// 因此编译会报错Add(50);return "call Add function sucess";}
}
单独将Add
两个函数编译是不会报错的:
但是调用它们其中一个的时候,编译会报错:
17.库合约
同其他语言里面的库函数,在Solidity
中还有个重要作用就是能够减少gas
;
和普通合约的区别:
- 不能有状态变量
- 不能够继承或被继承
- 不能接收以太币
- 不可以被销毁
库合约中的函数若被设置为public
或external
,则在调用函数时会触发一次delegatecall
;
若被设置为internal
,则不会触发;
若被设置为private
,由于是私人的,只能库合约内部自己访问;
常用的一些库合约:
- Strings:将
uint256
转为string
;- Address:判断某个地址是否为合约地址;
- Create2:更安全的使用
Create2 EVM opcode
;- Arrays:跟数组相关的库合约;
17_01.Strings库合约
此库合约是将uint256
类型转换为相应的string
类型的代码库,样例代码:
library Strings {bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";/*** @dev Converts a `uint256` to its ASCII `string` decimal representation.*/function toString(uint256 value) public pure returns (string memory) {// Inspired by OraclizeAPI's implementation - MIT licence// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.solif (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);}/*** @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.*/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);}/*** @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed 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);}
}
主要包含两个函数:
toString()
:将uint256
转换为string
;toHexString()
:将uint256
转换为hex
,再转换为string
;
17_02.使用库合约
有两种使用的方式;
- 使用
using A for B
;
为类型B
添加库合约A
;添加完后,B
类型变量的成员便自动添加了库A
中的函数,可以直接调用;
调用时,这个变量会被当作第一个参数传递给函数;
- 通过库合约名称来直接调用函数;
比如:Strings.toString(xxx);
;
示例:
contract A{// 使用using A for Busing Strings for uint256;function getString_1(uint256 num) public pure returns (string memory){return num.toString();}// 通过库合约名来调用function getString_2(uint256 num) public pure returns (string memory){return Strings.toHexString(num);}
}
18.Import
import
可以在一个文件中引用另一个文件的内容,提高代码的可重用性和组织性;
- 通过文件的相对位置可以引用:
import './xxx.sol';
; - 通过源文件网址导入网上的合约全局符号;
import 'https://xxxxx/xxx.sol';
; - 通过
npm
的目录导入:import '@openzeppelin/contracts/access/Ownable.sol';
; - 通过指定
全局符号
导入合约特定的全局符号:import {XXX} from './xxx.sol';
;
// ---------------------------Demo.sol----------------------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract B{function sayHello() public pure returns (string memory){return "Hello!";}
}// ---------------------------test.sol----------------------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 通过文件相对位置import
import './Demo.sol';
// 通过`全局符号`导入特定的合约
// 'B'是Demo.sol中合约的名称
import {B} from './Demo.sol';
// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
// 引用OpenZeppelin合约
import '@openzeppelin/contracts/access/Ownable.sol';contract A {// 成功导入Address库using Address for address;// 声明Demo.sol中的合约变量// 要使用合约名称B b = new B();// 调用引入的Demo.sol中合约B的函数function callImport() public view {b.sayHello();}
}
19.接收ETH
Solidity
支持两种特殊的回调函数:receive()
和fallback()
;
主要在两种情况使用:
- 接收ETH
- 处理合约中不存在的函数调用(代理合约
proxy contract
)
在0.6.x版本之前,语法上只有
fallback()
函数,用来接收用户发送的ETH以及在被调用函数签名没有匹配到时调用;0.6版本之后,Solidity才将其拆分为
receive()
和fallback()
。
19_01.接收ETH函数-receive
receive
函数是在合约收到ETH转账时会被调用的函数,一个合约最多只能有一个;
声明的方式和一般函数不一样,不需要function
关键字,且不能有任何参数,不能返回任何值,必须包含external
和payable
;
receive
函数最好不要执行太多逻辑,因为对方调用send
和transfer
方法发送ETH的话,gas
会被限制在2300,receive
太复杂可能会触发Out of gas
报错;用
call
就可以自定义gas
执行更复杂的逻辑。
示例(在receive
中发送一个事件):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract A {event Received(address Sender, uint Value);receive() external payable { emit Received(msg.sender, msg.value);}
}
在老版本中,有些恶意合约,会在
receive
函数中嵌入恶意消耗gas
的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作;
19_02.回退函数-fallback
fallback
函数会在调用合约中不存在的函数时被触发;
可用于接收ETH,也可用于代理合约(proxy contract
);
同receive
函数一样,不需要function
关键字,但必须包含external
,一般也会使用payable
来修饰;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract A {event fallbackCalled(address Sender, uint Value, bytes Data);fallback() external payable { emit fallbackCalled(msg.sender, msg.value, msg.data);}
}
19_03.两者区别
首先,它们俩都能够接收ETH;,它们触发的规则如下:
/ 是 --> receive()
/ 是 --> receive()是否存在
接收ETH --> msg.data是否为空 \ 否 --> fallback()
\ 否 --> fallback()
只有
msg.data
为空且receive()
存在时,才会使用receive()
;两者都不存在时,向合约发送ETH会报错;(但仍然可以通过带有
payable
的函数向合约发送ETH)
有receive
函数时,转账时data
为空:
转账时data
不为空:
20.发送ETH
Solidity
有三种方式向其他合约发送ETH:transfer()
,send()
,call()
,其中call
推荐使用;
首先先部署一个接收ETH的合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract ReceiveETH {// 收到ETH的事件// 记录amount和gasevent Log(uint amount, uint gas);// 接收ETH时触发的方法receive() external payable { emit Log(msg.value, gasleft());}// 返回ETH余额function getBalance() public view returns (uint){return address(this).balance;}
}
部署后运行getBalance()
,发现此时的余额为0:
20_01.transfer
用法:接收方地址.transfer(发送的ETH数额)
;
transfer
的gas限制是2300,足够用于转账,前提是接收方的fallback
和receive
不能太复杂;transfer
如果转账失败,会自动revert
交易(回滚交易);
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Transfer_test{function transferETH(address payable to, uint256 amount) external payable {// to是接收方to.transfer(amount);}
}
转账失败时:
转账成功时(多余的转账会被返回到发送方合约,并非附带ETH的钱包):
20_02.send
用法:接收方地址.send(发送的ETH数额)
;
send
的gas限制同样是2300;send
如果转账失败,不会revert
;send
的返回值是bool
,代表的是转账成功或者失败,需要额外的代码来处理;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Send_test{// 发送ETH失败的错误error SendFailed();// 发送ETHfunction sendETH(address payable to, uint256 amount) external payable {bool success = to.send(amount);if (!success){// 失败就revert错误revert SendFailed();}}
}
转账失败:
转账成功(同样多余的ETH退回到发送方合约):
20_03.call
用法:接收方地址.call{value:发送到ETH数额}("")
;
call
没有gas限制,可以支持对方合约fallback
和receive
实现复杂逻辑;call
如果转账失败,不会revert
;call
对返回值是bool
,和send
一样需要额外代码处理;
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Call_test{// 发送ETH失败的错误error CallFailed();// 发送ETHfunction callETH(address payable to, uint256 amount) external payable {bool success = to.call{value:amount}("");if (!success){// 失败就revert错误revert CallFailed();}}
}
转账失败时:
转账成功时(同样多余的ETH退回到发送方合约):
21.调用其他合约
TestContract
合约,目的是被其他合约所调用:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract TestContract {// 设置私有变量uint256 private x = 0;// 交易的事件,记录amount和gasevent Log(uint amount, uint gas);// 得到合约账户余额function getBalance() public view returns (uint){return address(this).balance;}// 设置合约中私有变量值// 同时可以向其中转账function setX(uint256 num) external payable {x = num;if(msg.value > 0){emit Log(msg.value, gasleft());}}// 获得私有变量的值function getX() external view returns (uint256){return x;}
}
部署,并得到合约地址:
调用合约的代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 由于在不同的文件,所以先导入
import './test.sol';contract CallContract{// 方式一// 合约名(合约地址).func()function callSetX(address contract_address, uint256 x) external{TestContract(contract_address).setX(x);}// 方式二// 合约地址.func()function callGetX(TestContract contract_address) external view returns(uint x){x = contract_address.getX();}// 方式三// 创建合约对象的方式,然后调用function callGetX_2(address contract_address) external view returns(uint x){TestContract tc = TestContract(contract_address);x = tc.getX();}// 调用并转账function setXTransferETH(address contract_address, uint256 x) payable external{TestContract(contract_address).setX{value: msg.value}(x);}
}
21_01.调用方式一
可以在函数中传入合约地址,生成目标合约的引用,然后再调用函数;
-
用法:
合约名(合约地址).func(参数)
; -
合约名和接口都必须保持一致(
TestContract
和setX()
);
// 方式一
// 合约名(合约地址).func()
function callSetX(address contract_address, uint256 x) external{TestContract(contract_address).setX(x);
}
21_02.调用方式二
参考方式一中,将address
类型换为目标合约名
即可;
注意:TestContract contract_address
的底层还是address
类型,生成的ABI
中,调用callGetX
时传入的参数都是address
类型的;
- 用法:
- 参数->
合约名 合约地址
; - 函数内部->
合约地址.func(参数)
;
- 参数->
// 方式二
// 合约地址.func()
function callGetX(TestContract contract_address) external view returns(uint x){x = contract_address.getX();
}
21_03.调用方式三
通过创建合约(对象)的方式;
用法:合约名 变量名 = 合约名(地址);
;
// 方式三
// 创建合约对象的方式,然后调用
function callGetX_2(address contract_address) external view returns(uint x){TestContract tc = TestContract(contract_address);x = tc.getX();
}
21_04.调用并转账
如果目标函数是payable
的,那么便可以向其转账;
用法:合约名(合约地址).func{value:xxx}(参数);
;
// 调用并转账
function setXTransferETH(address contract_address, uint256 x) payable external{TestContract(contract_address).setX{value: msg.value}(x);
}
22.Call
在20_03
中call
可以用来发送ETH,同时它还可以调用合约;
call
是address
类型的低级成员函数,它用来与其他合约交互;
- 返回值:
(bool, bytes memory)
,分别对应call
是否成功以及目标函数的返回值;
call
是官方推荐的通过触发fallback
或receive
函数发送ETH的方法;- 不推荐用
call
来调用另一个合约(因为当你调用一个不安全的合约时,主动权便不在你的手上;推荐声明合约变量后调用函数21_03
);- 当我们不知道对方合约的源代码或者
ABI
,就没法生成合约变量;此时,仍然可以通过call
调用对方合约的函数;
22_01.使用规则
用法:目标合约地址.call(字节码)
,可以在不知道源代码或ABI的情况下调用;
字节码
:利用结构化编码函数来获得 -->abi.encodeWithSignature("函数签名", 具体参数, 具体参数, ...)
;函数签名
:是函数名(参数类型,参数类型,...)
示例:
abi.encodeWithSignature("f(uint256,address)",x,addr)
在调用合约的同时,call
还能知道交易发送的ETH和gas:
使用方法:目标合约地址.call{value:ETH数额, gas:gas数额}(字节码)
,就是在参数前加了大括号,里面填上发送的数额;
22_02.通过call调用目标合约
目标合约(还是和之前一样):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract TestContract {// 设置私有变量uint256 private x = 0;// 交易的事件,记录amount和gasevent Log(uint amount, uint gas);// 得到合约账户余额function getBalance() public view returns (uint){return address(this).balance;}// 设置合约中私有变量值// 同时可以向其中转账function setX(uint256 num) external payable {x = num;if(msg.value > 0){emit Log(msg.value, gasleft());}}// 获得私有变量的值function getX() external view returns (uint256){return x;}
}
调用setX(uint256 num)
函数,有参数,但无返回值(data
无内容),附带ETH发送过去:
调用getX()
函数,无参数,但有返回值(data
有内容),不带ETH:
调用一个不存在的函数:
- 当没有
fallback
函数的情况下(会返回false):
- 当给目标合约添加一个
fallback
函数时,再调用它(会返回true):
fallback() external payable { }
完整示例代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 没有导入test.sol
contract CallContract{// 定义的Response事件// 输出call返回的结果和dataevent Response(bool success, bytes data);function callSetX(address payable addr, uint256 x) public payable {// 调用setX函数// 同时可以发送ETH// {}中是发送的ETH数额// ()中是利用结构化编码函数获得的字节码(bool success, bytes memory data) = addr.call{value:msg.value}(abi.encodeWithSignature("setX(uint256)", x));emit Response(success, data);}function callGetX(address addr) external returns (uint256){// 调用getX函数// ()中是利用结构化编码函数获得的字节码(bool success, bytes memory data) = addr.call(abi.encodeWithSignature("getX()"));emit Response(success, data);// 返回data中的值(转为uint)return abi.decode(data, (uint256));}function callNonExist(address addr) external {// 调用一个不存在的函数(bool success, bytes memory data) = addr.call(abi.encodeWithSignature("xxx(address)"));emit Response(success, data);}
}
23.DelegateCall
delegatecall
委托,和call
差不多,同样是地址类型的低级成员函数;
23_01.什么是委托
当用户A
通过合约B
来call
合约C
时:
-
此时执行的是合约
C
上的函数; -
上下文(
Context
,可以理解为包含变量和状态的环境)也是合约C
的:-
msg.sender
是合约B
的地址 -
若函数改变了一些状态变量,产生的效果会用在合约
C
的变量上;
-
而当用户A
通过合约B
来delegatecall
合约C
时:
- 执行的是合约
C
上的函数; - 上下文仍然是合约
B
的:msg.sender
是合约A
的地址;- 若函数改变了一些状态变量,产生的效果会用在合约
B
的变量上;
也可以这么理解:
- 合约B的视角
我合约B"借用"了合约C的某一个函数的功能,来改变我自己这边的一些状态;
- 现实世界
用户A
:投资者
合约B中的状态变量
:资产
合约C中执行的函数
:风险投资机构
投资者
将他的资产
交给一个风险投资机构
来打理,此时执行的是风险投资机构
,但改变的是投资者
的资产
;
23_02.使用规则
和call
类似:目标合约地址.delegatecall(字节码)
;
其中字节码
仍是通过abi.encodeWithSignature()
来获得的;
与call
不一样的是:delegatecall()
在调用时,不能指定发送的ETH
数额,但能指定gas
数额;
注意:
delegatecall()
有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成财产损失。
23_03.什么情况下用到委托
主要有两个应用场景:
- 代理合约(
Proxy Contract
)
将智能合约的存储合约
和逻辑合约
分开;
存储合约(代理合约(Proxy Contract)
)存储所有相关的变量,并且保存逻辑合约的地址;
逻辑合约(Logic Contract
)中存储所有的函数,通过delegatecall
执行;
当升级的时候,只需要将代理合约指向新的逻辑合约即可(以太坊官方开发文档中有提到)。
EIP-2535 Diamonds
(钻石)
钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石具有多个实施合约的代理合约。详细信息:钻石标准简介。
23_04.示例
用户A
通过合约B
委托调用合约C
;
被调用的合约C
两个状态变量和一个可以修改状态变量的函数:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 被调用的合约C
contract C {// 状态变量numuint public num;// 状态变量senderaddress public sender;// 设置状态变量num和sender的值function setVars(uint x) public payable {num = x;sender = msg.sender;}
}
发起调用的合约B
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 没有导入test.sol
contract B{// 必须与合约C的变量存储布局相同// 两个变量,顺序也必须一致uint public num;address public sender;// 通过call来调用SetVars函数// 预计只会改变合约C的变量值function callSetVars(address addr, uint x) external payable {(bool success, bytes memory data) = addr.call(abi.encodeWithSignature("setVars(uint256)", x));}// 通过delegatecall来调用SetVars函数// 预计只改变本合约(合约B)的变量值function delegatecallSetVars(address addr, uint x) external payable {(bool success, bytes memory data) = addr.delegatecall(abi.encodeWithSignature("setVars(uint256)", x));}
}
验证
状态变量的初始值:
在合约B中
调用callSetVars
函数,预计只会改变合约C
中的变量值(num
为更改后的值,sender
为合约B的地址):
在合约B中调用delegatecallSetVars函数,预计会改变合约B中的变量(num
变为更改后的值,sender
为钱包地址),合约C中的不变:
24.在合约中创建新合约
以太坊上,外部账户EOA
(钱包)可以创建智能合约;此外,智能合约也可以创建新的智能合约。
去中心化交易所
Uniswap
就是利用工厂合约(PairFactory)
创建了无数个币对合约(Pair)
。
Uniswap V2
核心合约中包含两个合约:
UniswapV2Pair
:币对合约,用于管理币对地址,流动性,买卖;UniswapV2Factory
:工厂合约,用于创建新的币对合约,并管理币对地址;
24_01.Create
Create
用法:ContractXXX xxx = new ContarctXXX{value:_value}(构造函数参数)
就和new
对象一样,新new
一个合约,并传入新合约构造函数
所需要的参数,并且可以附带ETH(前提构造函数得是payable
的);
极简Uniswap
用Create
来实现一个极简版的Uniswap
(真正的Uniswap
不是用这种方式实现的,是24_02
中的方法):
币对合约(Pair)
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 币对合约
// 包含3个状态变量
// 部署时将factory赋值
// 调用initToken时更新币对中两个代币的地址
contract Pair{// 工厂地址address public factory;// 代币0address public token0;// 代币1address public token1;// 构造函数,带有payable// 将消息的发送者赋值为factoryconstructor() payable {factory = msg.sender;}// 初始化代币0和代币1的地址function initToken(address _token0, address _token1) external {// 检测是否是factory调用的require(factory == msg.sender, "Not real factory use function");// 代币地址赋值token0 = _token0;token1 = _token1;}
}
工厂合约(PairFactory)
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import './Pair.sol';// 工厂合约
// 一个映射将 代币 和 币对合约地址 建立联系
// 一个数组 保存 币对合约地址
// 利用Create方法创建新的合约
contract PairFactory {// 映射,address -> address -> addressmapping (address => mapping ( address => address )) public getPair;// 保存所有的Pair地址(币对合约地址)address[] public allPairs;// 创建新的币对合约地址function createPair(address token0, address token1) external returns (address pairAddr){// 利用Create方法创建新合约Pair pair = new Pair();// 调用新合约的initToken方法,并初始化里面的token0,token1pair.initToken(token0, token1);// 获得当前币对合约的地址pairAddr = address(pair);// 保存在数组中allPairs.push(pairAddr);// 建立映射// token0 -> token1 -> 币对合约地址getPair[token0][token1] = pairAddr;// token1 -> token0 -> 币对合约地址getPair[token1][token0] = pairAddr;}
}
利用下面两个地址作为参数调用createPair
函数:
WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
24_02.Create2
上面可以看到Create
方法创建的合约地址是完全不可预测的;
而Create2
方法使我们在部署智能合约之前就能预测合约的地址(Uniswap
创建 Pair合约(币对合约)
的方法就是这个)。
Create2
方法的目的是为了让合约地址独立于未来事件,不管未来区块链上发生什么,都可以将合约部署在事先计算好的地址上。
Create原理
新地址 = hash(创建者地址, nonce)
;无论是
EOA
创建还是智能合约创建,都是这个方法;
创建者地址
是部署的钱包地址
或者合约地址
;
nonce
,对于EOA
是该地址发送的交易总数,对于合约账户
是创建的合约总数,创建时的nonce
为nonce+1
;创建者的地址不会变,但是
nonce
会随着时间而改变,所以不好预测;
Create2原理
新地址 = hash("0xFF", 创建者地址, salt, initcode)
;
0xFF
:一个常数,避免和Create
冲突;
创建者地址
:调用Create2
的当前合约地址;
salt
:一个由创建者指定的bytes32
类型的值,主要目的是用来影响新创建的合约地址;
initcode
:新合约的初始字节码(合约的Creation Code和构造函数参数);
Create2用法
ContractXXX xxx = new COntractXXX{salt:_salt, value:_value}(构造函数参数)
;
同样也是new
,只不过多加入了个salt
;
极简Uniswap2
使用Create2
来实现一个极简的Uniswap
;
币对合约(Pair)
(和之前一样):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 币对合约
// 包含3个状态变量
// 部署时将factory赋值
// 调用initToken时更新币对中两个代币的地址
contract Pair{// 工厂地址address public factory;// 代币0address public token0;// 代币1address public token1;// 构造函数,带有payable// 将消息的发送者赋值为factoryconstructor() payable {factory = msg.sender;}// 初始化代币0和代币1的地址function initToken(address _token0, address _token1) external {// 检测是否是factory调用的require(factory == msg.sender, "Not real factory use function");// 代币地址赋值token0 = _token0;token1 = _token1;}
}
工厂合约(PairFactory)
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import './Pair.sol';// 工厂合约
// 一个映射将 代币 和 币对合约地址 建立联系
// 一个数组 保存 币对合约地址
// 利用Create方法创建新的合约
contract PairFactoryV2 {// 映射,address -> address -> addressmapping (address => mapping ( address => address )) public getPair;// 保存所有的Pair地址(币对合约地址)address[] public allPairs;// 创建新的币对合约地址function createPairV2(address token0, address token1) external returns (address pairAddr){// 检测两个地址不同require(token0 != token1, "Identcial Address");// 将地址按照从小到大排序(address token_0, address token_1) = token0 < token1 ? (token0, token1) : (token1, token0);// 计算一个saltbytes32 salt = keccak256(abi.encodePacked(token_0, token_1));// 利用Create2方法创建新合约Pair pair = new Pair{salt: salt}();// 调用新合约的initToken方法,并初始化里面的token0,token1pair.initToken(token_0, token_1);// 获得当前币对合约的地址pairAddr = address(pair);// 保存在数组中allPairs.push(pairAddr);// 建立映射// token0 -> token1 -> 币对合约地址getPair[token0][token1] = pairAddr;// token1 -> token0 -> 币对合约地址getPair[token1][token0] = pairAddr;}// 预测地址function calcAddr(address token0, address token1) public view returns (address predictedAddr){// 检测两个地址不同require(token0 != token1, "Identcial Address");// 将地址按照从小到大排序(address token_0, address token_1) = token0 < token1 ? (token0, token1) : (token1, token0);// 计算一个saltbytes32 salt = keccak256(abi.encodePacked(token_0, token_1));// 计算地址predictedAddr = address(uint160(uint(// hashkeccak256(abi.encodePacked(// 四个参数bytes1(0xff),address(this),salt,keccak256(type(Pair).creationCode))))));}
}
若部署的合约的构造函数中需要有参数:
比如
Pair pair new Pair{salt:salt}(address(this));
predictedAddr = address(uint160(uint(// hashkeccak256(abi.encodePacked(// 四个参数bytes1(0xff),address(this),salt,// 一起打包,并计算哈希keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))))) ));
还是利用这两个地址:
WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
事先计算:
验证:
24_03.应用场景
- 交易所为新用户预留创建钱包合约的地址;
- 减少不必要的调用(知道新合约的地址后,无需再执行
getPair
的跨合约调用);
25.删除合约
25_01.selfdestruct
selfdestruct
命令可被用来删除合约,并将该合约剩余的ETH转到指定地址;
它为了应对合约出错的极端情况而设计的,最早被命名为
suicide
,后面改为selfdestruct
;在
v0.8.18
版本中,它被标记为"不再建议使用",因为在一些情况下它会导致预期之外的合约语意,但由于目前还没有替代方案,只对开发者做了编译阶段的警告,相关内容:EIP-6049。然而,在以太坊坎昆(Cancun)升级中,EIP-6780被纳入升级以实现对
Verkle Tree
更好的支持。该更新减少了SELFDESTRUCT
操作码的功能。根据提案描述,当前
SELFDESTURCT
仅会被用来将合约中的ETH转移到指定地址,而原先的删除功能只有在合约创建-自毁
这两个操作处在同一笔交易时才能生效。
所以,目前来说:
- 现在的
seldestrict
仅会被用来将合约中的ETH转移到指定地址; - 已经部署的合约无法被
SELFDESTRUCT
; - 如果要使用原先的
SELFDESTRUCT
功能,必须在同一笔交易中创建并自毁;
25_02.如何使用selfdeftruct
用法:selfdestruct(addr)
;
其中,
addr
是接收合约中剩余ETH的地址,并且addr
地址不需要有receive()
或fallback()
也能接收ETH。
25_03.升级前后功能对比
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;contract SelfDestructDemo{uint public value = 10;constructor() payable {}receive() external payable { } // 升级之前应该能自毁// 升级之后只能转移ETHfunction SelfDestruct() external {selfdestruct(payable(msg.sender));}// 获取余额function getBalance() external view returns (uint balance){balance = address(this).balance;}
}
升级前:
合约中函数报错,并且合约中的ETH被转入指定地址;
升级后:
合约中的ETH被转入指定地址,但合约中的函数仍能使用;
25_04.同笔交易实现创建-自毁
// SPDX-License-Identifier: MIT
// pragma solidity ^0.8.26;
pragma solidity ^0.8.4;// DeployDestructDemo合约(还是上一个)
import './Factory.sol';contract DeployDestructDemo{struct DemoResult{address addr;uint balance;uint value;}constructor() payable {}function getBalance() external view returns (uint balance){balance = address(this).balance;}// 演示创建-自毁function demo() public payable returns (DemoResult memory){// 创建一个新合约SelfDestructDemo sd = new SelfDestructDemo{value:msg.value}();// 给返回值赋值DemoResult memory res = DemoResult({addr:address(sd),balance:sd.getBalance(),value:sd.value()});// 新合约调用自销毁sd.SelfDestruct();return res;}
}
26.ABI编码解码
ABI
-(Application Binary Interface,应用二进制接口),是与以太坊智能合约交互的标准。
数据基于他们的类型编码,并且由于编码后不包含类型信息,解码时需要注明它们的类型;
编码:
abi.encode
、abi.encodePacked
、abi.encodeWithSignature
、abi.encodeWithSelector
;解码:
abi.decode
;
26_01.abi编码
下面将这4个变量一起打包编码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract ABIEncode{uint256 x = 10;address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;string name = "0xAA";uint[2] array = [3, 4];function encode() public view returns (bytes memory res){res = abi.encodeXXX(x, addr, name, array);}
}
abi.encode(能和合约交互)
将给定参数利用ABI规则编码;
将每个参数填充为32字节的倍数的数据,并拼接在一起;
如果要和智能合约交互,需要使用它;
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
// 结果
//0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/
若将string变成很长:
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
uint[2] array = [3, 4];
// 结果
//0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000047307841414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414100000000000000000000000000000000000000000000000000
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000047(string)
3078414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141
4141414141414100000000000000000000000000000000000000000000000000
*/
abi.encodePacked(不能和合约交互)
将给定参数根据其所需要的最低空间编码,与abi.encode
类似,但会省略很多0;
但不能与合约交互;
uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
// 结果
//0x000000000000000000000000000000000000000000000000000000000000000a5b38da6a701c568545dcfcb03fcb875f56beddc43078414100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x,因为是uint256)
5b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
30784141(string)
00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004(array)
*/
abi.encodeWithSignature(调用其他合约时使用)
与abi.encode
类似,但是第一个参数是函数签名
,keccak
哈希,编码时为4字节,等同于在前面加了个函数选择器
;
当调用其他函数的时候可以使用;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract ABIEncode{uint256 x = 10;address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;string name = "0xAA";uint[2] array = [3, 4];function encode() public view returns (bytes memory res){res = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);}
}// 结果
//0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
e87082f1(函数签名)
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/
abi.encodeWithSelector
与abi.encodeWithSignature
类似,只不过第一个参数时函数选择器
,为函数签名Keccak
哈希的前4个字节;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract ABIEncode{uint256 x = 10;address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;string name = "0xAA";uint[2] array = [3, 4];function encode() public view returns (bytes memory res){res = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);}
}// 结果
//0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
e87082f1(函数签名)
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/
26_02.abi解码
abi.decode
用于解码abi.encode
生成的二进制编码,将它还原成原本的参数;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract ABIEncode{function dncode(bytes memory data) public pure returns (uint x, address addr, string memory name, uint[2] memory array){(x, addr, name, array) = abi.decode(data, (uint, address, string, uint[2]));}
}
// 输入
// 0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
27.选择器
27_01.calldata
当我们调用智能合约时,本质上是向目标合约发送了一段calldata
,发送交易后,可以在详细信息的input
中看到此次交易的calldata
:
发送的calldata
中前4个字节是函数选择器(selector)
;
// 上图中的calldata
// 0x012b48bf000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
012b48bf(函数选择器)
(因为bytes是动态的,所以会有下面这俩,静态的不会有,比如address,uint)
0000000000000000000000000000000000000000000000000000000000000020(偏移量,0x20 = 32,从这开始偏移32个字节)
00000000000000000000000000000000000000000000000000000000000000e0(参数长度,0xe0 = 7 * 32,正好对应上面)输入的参数
000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
*/
其实,calldata
就是告诉智能合约,为要调用哪个函数,参数都是什么;
27_02.selector的生成
基础类型参数
基础类型的参数有:uint(uint8, ..., uint256)
、bool
、address
等;
bytes(keccak256("func_name(uint256,bool,...)"));
固定长度类型参数
固定长度类型的参数,比如:uint256[3]
;
bytes(keccak256("func_name(uint256[3])"));
可变长度类型参数
可变长度类型的参数,比如:address[]
、uint[]
、string
、bytes
等;
bytes(keccak256("func_name(bytes,string)"));
映射类型参数
映射类型的参数有:contract
、enum
、struct
等;
contract Demo{} // 需要转化为address
struct User{ // 需要转化为tuple类型:(uint256,bytes)uint256 uid;bytes name;
}
enum School {SCHOOL1, SCHOOL2} // 需要转化为uint8
mapping(address => uint) public balance; // 直接转化为address(第一个类型),因为mapping类型不能直接作为参数
bytes(keccak256("func_name(address,(uint256,bytes),uint256[],uint8),address"))
27_03.使用selector
address(this).call(abi.encodeWithSelector(0x12345678函数签名, 参数, 参数, ...));
28.Try Catch
28_01.用法
基础用法
try func_name(){// call成功的情况下
} catch{// call失败的情况下
}
调用的函数有返回值
必须这么使用(需要加上returns
),同时可以使用返回的变量:
try func_name() returns (address addr, uint x){// call成功的情况下// 可以使用返回的变量
} catch{// call失败的情况下
}
捕捉特殊的异常原因
try func_name() returns (address addr, uint x){// call成功的情况下// 可以使用返回的变量
} catch Error(string memory reason){// 捕捉revert("xxxx")// 捕捉require(false, "xxxx")
} catch Panic(uint errorCode){// 捕捉Panic导致的错误// 例如assert失败、溢出、除零、数组访问越界等
} catch (bytes memory lowLevelData){// 如果发生了revert且上面2个异常匹配失败,会进入这个分支// 例如revert()、require(false)、revert(自定义的error)
}
28_02.示例
调用合约(合约创建成功,但函数调用错误)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract OnlyEven{constructor(uint a){// 当a = 0时,require会抛出异常require(a != 0, "invalid number");// 当a = 1时,assert会抛出异常assert(a != 1);}function onlyEven(uint b) external pure returns(bool success){// 当b为奇数时,require抛出异常require(b % 2 == 0, "Odd number");success = true;}
}contract TryCatch{// 成功事件event SuccessEvent();// 抛出异常时的两个事件// 对应require和revertevent CatchEvent(string message);// 对应assertevent CatchByte(bytes data);// 合约状态变量OnlyEven oe;// 构造函数constructor(){// 赋值为2,应该不会抛出异常oe = new OnlyEven(2);}function exec(uint amount) external returns (bool success){try oe.onlyEven(amount) returns (bool _success){// 成功,返回Trueemit SuccessEvent();return _success;} catch Error(string memory reason){// 失败,捕捉require(false, error_string)// 比如此处输入的是奇数,应该返回"Odd number"emit CatchEvent(reason);}}
}
成功:
失败:
调用合约(合约创建失败)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract OnlyEven{constructor(uint a){// 当a = 0时,require会抛出异常require(a != 0, "invalid number");// 当a = 1时,assert会抛出异常assert(a != 1);}function onlyEven(uint b) external pure returns(bool success){// 当b为奇数时,require抛出异常require(b % 2 == 0, "Odd number");success = true;}
}contract TryCatch{// 成功事件event SuccessEvent();// 抛出异常时的两个事件// 对应require和revertevent CatchEvent(string message);// 对应assertevent CatchByte(bytes data);// exec(0) --> 失败,释放CatchEvent// exec(1) --> 失败,释放CatchByte// exec(2) --> 成功,释放SuccessEventfunction exec(uint num) external returns (bool success){try new OnlyEven(num) returns (OnlyEven oe){emit SuccessEvent();success = oe.onlyEven(num);} catch Error(string memory reason){// 捕捉失败的revert()和require()emit CatchEvent(reason);} catch (bytes memory reason){// 捕捉失败的assert()emit CatchByte(reason);}}
}
exec(0) --> 失败,释放CatchEvent:
exec(1) --> 失败,释放CatchByte:
exec(2) --> 成功,释放SuccessEvent:
参考:https://github.com/AmazingAng/WTF-Solidity