Solidity学习笔记-2
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,输出string
function 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;
// 传入参数是uint8
function Add(uint8 num) public {
data += num;
}
// 传入参数是uint256
function 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,由于是私人的,只能库合约内部自己访问;
常用的一些库合约:
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.sol
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);
}
/**
* @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 B
using 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;,它们触发的规则如下:

只有
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和gas
event 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();
// 发送ETH
function 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();
// 发送ETH
function 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和gas
event 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和gas
event 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返回的结果和data
event 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 {
// 状态变量num
uint public num;
// 状态变量sender
address 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;
// 代币0
address public token0;
// 代币1
address public token1;
// 构造函数,带有payable
// 将消息的发送者赋值为factory
constructor() 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 -> address
mapping (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,token1
pair.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;
// 代币0
address public token0;
// 代币1
address public token1;
// 构造函数,带有payable
// 将消息的发送者赋值为factory
constructor() 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 -> address
mapping (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);
// 计算一个salt
bytes32 salt = keccak256(abi.encodePacked(token_0, token_1));
// 利用Create2方法创建新合约
Pair pair = new Pair{salt: salt}();
// 调用新合约的initToken方法,并初始化里面的token0,token1
pair.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);
// 计算一个salt
bytes32 salt = keccak256(abi.encodePacked(token_0, token_1));
// 计算地址
predictedAddr = address(uint160(uint(
// hash
keccak256(abi.encodePacked(
// 四个参数
bytes1(0xff),
address(this),
salt,
keccak256(type(Pair).creationCode)
)))
));
}
}
若部署的合约的构造函数中需要有参数:
比如
Pair pair new Pair{salt:salt}(address(this));predictedAddr = address(uint160(uint( // hash keccak256(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 { }
// 升级之前应该能自毁
// 升级之后只能转移ETH
function 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和revert
event CatchEvent(string message);
// 对应assert
event 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){
// 成功,返回True
emit 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和revert
event CatchEvent(string message);
// 对应assert
event CatchByte(bytes data);
// exec(0) --> 失败,释放CatchEvent
// exec(1) --> 失败,释放CatchByte
// exec(2) --> 成功,释放SuccessEvent
function 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:

浙公网安备 33010602011771号