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;
    }
}
image-20241117163811702

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两个函数编译是不会报错的:

image-20241117164944148

但是调用它们其中一个的时候,编译会报错:

image-20241117165110394

17.库合约

同其他语言里面的库函数,在Solidity中还有个重要作用就是能够减少gas

和普通合约的区别:

  1. 不能有状态变量
  2. 不能够继承或被继承
  3. 不能接收以太币
  4. 不可以被销毁

库合约中的函数若被设置为publicexternal,则在调用函数时会触发一次delegatecall

若被设置为internal,则不会触发;

若被设置为private,由于是私人的,只能库合约内部自己访问;

常用的一些库合约:

  1. Strings:将uint256转为string
  2. Address:判断某个地址是否为合约地址;
  3. Create2:更安全的使用Create2 EVM opcode
  4. 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.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);
    }
}

主要包含两个函数:

  1. toString():将uint256转换为string
  2. toHexString():将uint256转换为hex,再转换为string

17_02.使用库合约

有两种使用的方式;

  1. 使用using A for B

为类型B添加库合约A;添加完后,B类型变量的成员便自动添加了库A中的函数,可以直接调用;

调用时,这个变量会被当作第一个参数传递给函数;

  1. 通过库合约名称来直接调用函数;

比如: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);
    }
}
image-20241117173523142

18.Import

import可以在一个文件中引用另一个文件的内容,提高代码的可重用性和组织性;

  1. 通过文件的相对位置可以引用:import './xxx.sol';
  2. 通过源文件网址导入网上的合约全局符号;import 'https://xxxxx/xxx.sol';
  3. 通过npm的目录导入:import '@openzeppelin/contracts/access/Ownable.sol';
  4. 通过指定全局符号导入合约特定的全局符号: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();
    }
}
image-20241118212707032

19.接收ETH

Solidity支持两种特殊的回调函数:receive()fallback()

主要在两种情况使用:

  1. 接收ETH
  2. 处理合约中不存在的函数调用(代理合约proxy contract

在0.6.x版本之前,语法上只有fallback()函数,用来接收用户发送的ETH以及在被调用函数签名没有匹配到时调用;

0.6版本之后,Solidity才将其拆分为receive()fallback()

19_01.接收ETH函数-receive

receive函数是在合约收到ETH转账时会被调用的函数,一个合约最多只能有一个;

声明的方式和一般函数不一样,不需要function关键字,且不能有任何参数不能返回任何值,必须包含externalpayable

receive函数最好不要执行太多逻辑,因为对方调用sendtransfer方法发送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;,它们触发的规则如下:

image-20250104141846535

只有msg.data为空且receive()存在时,才会使用receive()

两者都不存在时,向合约发送ETH会报错;(但仍然可以通过带有payable的函数向合约发送ETH)

receive函数时,转账时data为空:

image-20241118222729536

转账时data不为空:

image-20241118223045330

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:

image-20241119225817417

20_01.transfer

用法:接收方地址.transfer(发送的ETH数额)

  • transfer的gas限制是2300,足够用于转账,前提是接收方的fallbackreceive不能太复杂;
  • 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);
    }
}

转账失败时:

image-20241119231200736

转账成功时(多余的转账会被返回到发送方合约,并非附带ETH的钱包):

image-20241119232141984

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();
        }
    }
}

转账失败:

image-20241119233240303

转账成功(同样多余的ETH退回到发送方合约):

image-20241119233450913

20_03.call

用法:接收方地址.call{value:发送到ETH数额}("")

  • call没有gas限制,可以支持对方合约fallbackreceive实现复杂逻辑;
  • 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();
        }
    }
}

转账失败时:

image-20241119234226065

转账成功时(同样多余的ETH退回到发送方合约):

image-20241119234359832

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;
    }
}

部署,并得到合约地址:

image-20241120002354682

调用合约的代码:

// 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(参数)

  • 合约名和接口都必须保持一致(TestContractsetX());

// 方式一
// 合约名(合约地址).func()
function callSetX(address contract_address, uint256 x) external{
    TestContract(contract_address).setX(x);
}
image-20241120003213411

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();
}
image-20241120003946647

21_03.调用方式三

通过创建合约(对象)的方式;

用法:合约名 变量名 = 合约名(地址);

// 方式三
// 创建合约对象的方式,然后调用
function callGetX_2(address contract_address) external view returns(uint x){
    TestContract tc = TestContract(contract_address);
    x = tc.getX();
}
image-20241120004228115

21_04.调用并转账

如果目标函数是payable的,那么便可以向其转账;

用法:合约名(合约地址).func{value:xxx}(参数);

// 调用并转账
function setXTransferETH(address contract_address, uint256 x) payable external{
    TestContract(contract_address).setX{value: msg.value}(x);
}
image-20241120004826076

22.Call

20_03call可以用来发送ETH,同时它还可以调用合约;

calladdress类型的低级成员函数,它用来与其他合约交互;

  • 返回值:(bool, bytes memory),分别对应call是否成功以及目标函数的返回值;
  • call是官方推荐的通过触发fallbackreceive函数发送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发送过去:

image-20241121220329742

调用getX()函数,无参数,但有返回值(data有内容),不带ETH:

image-20241121221156903

调用一个不存在的函数:

  • 当没有fallback函数的情况下(会返回false):
image-20241121222026282
  • 当给目标合约添加一个fallback函数时,再调用它(会返回true):
fallback() external payable { }
image-20241121222709235

完整示例代码:

// 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通过合约Bcall合约C时:

  • 此时执行的是合约C上的函数;

  • 上下文(Context,可以理解为包含变量和状态的环境)也是合约C的:

    • msg.sender是合约B的地址

    • 若函数改变了一些状态变量,产生的效果会用在合约C的变量上;

image-20241121223728699

而当用户A通过合约Bdelegatecall合约C时:

  • 执行的是合约C上的函数;
  • 上下文仍然是合约B的:
    • msg.sender是合约A的地址;
    • 若函数改变了一些状态变量,产生的效果会用在合约B的变量上;
image-20241121223807749

也可以这么理解:

  1. 合约B的视角

我合约B"借用"了合约C的某一个函数的功能,来改变我自己这边的一些状态;

  1. 现实世界

用户A:投资者

合约B中的状态变量:资产

合约C中执行的函数:风险投资机构

投资者将他的资产交给一个风险投资机构来打理,此时执行的是风险投资机构,但改变的是投资者资产

23_02.使用规则

call类似:目标合约地址.delegatecall(字节码)

其中字节码仍是通过abi.encodeWithSignature()来获得的;

call不一样的是:delegatecall()在调用时,不能指定发送的ETH数额,但能指定gas数额;

注意:delegatecall()有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成财产损失。

23_03.什么情况下用到委托

主要有两个应用场景:

  1. 代理合约(Proxy Contract

将智能合约的存储合约逻辑合约分开;

存储合约(代理合约(Proxy Contract))存储所有相关的变量,并且保存逻辑合约的地址;

逻辑合约(Logic Contract)中存储所有的函数,通过delegatecall执行;

当升级的时候,只需要将代理合约指向新的逻辑合约即可(以太坊官方开发文档中有提到)。

  1. 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));
    }
}

验证

状态变量的初始值:

image-20241121235657671

合约B中调用callSetVars函数,预计只会改变合约C中的变量值(num为更改后的值,sender为合约B的地址):

image-20241122000241525

在合约B中调用delegatecallSetVars函数,预计会改变合约B中的变量(num变为更改后的值,sender为钱包地址),合约C中的不变:

image-20241122000953665

24.在合约中创建新合约

以太坊上,外部账户EOA(钱包)可以创建智能合约;此外,智能合约也可以创建新的智能合约。

去中心化交易所Uniswap就是利用工厂合约(PairFactory)创建了无数个币对合约(Pair)

Uniswap V2核心合约中包含两个合约:

  1. UniswapV2Pair:币对合约,用于管理币对地址,流动性,买卖;
  2. 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
image-20241126002026701 image-20241126002224011

24_02.Create2

上面可以看到Create方法创建的合约地址是完全不可预测的;

Create2方法使我们在部署智能合约之前就能预测合约的地址(Uniswap创建 Pair合约(币对合约)的方法就是这个)。

Create2方法的目的是为了让合约地址独立于未来事件,不管未来区块链上发生什么,都可以将合约部署在事先计算好的地址上。

Create原理

新地址 = hash(创建者地址, nonce)

无论是EOA创建还是智能合约创建,都是这个方法;

创建者地址部署的钱包地址或者合约地址

nonce,对于EOA是该地址发送的交易总数,对于合约账户是创建的合约总数,创建时的noncenonce+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

事先计算:

image-20241126012744690

验证:

image-20241126012839407 image-20241126013109845

24_03.应用场景

  1. 交易所为新用户预留创建钱包合约的地址;
  2. 减少不必要的调用(知道新合约的地址后,无需再执行getPair的跨合约调用);

25.删除合约

25_01.selfdestruct

selfdestruct命令可被用来删除合约,并将该合约剩余的ETH转到指定地址;

它为了应对合约出错的极端情况而设计的,最早被命名为suicide,后面改为selfdestruct

v0.8.18版本中,它被标记为"不再建议使用",因为在一些情况下它会导致预期之外的合约语意,但由于目前还没有替代方案,只对开发者做了编译阶段的警告,相关内容:EIP-6049

然而,在以太坊坎昆(Cancun)升级中,EIP-6780被纳入升级以实现对Verkle Tree更好的支持。该更新减少了SELFDESTRUCT操作码的功能。

根据提案描述,当前SELFDESTURCT仅会被用来将合约中的ETH转移到指定地址,而原先的删除功能只有在合约创建-自毁这两个操作处在同一笔交易时才能生效。

所以,目前来说:

  1. 现在的seldestrict仅会被用来将合约中的ETH转移到指定地址;
  2. 已经部署的合约无法被SELFDESTRUCT
  3. 如果要使用原先的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被转入指定地址;

image-20241127002142455 image-20241127002254054

升级后:

合约中的ETH被转入指定地址,但合约中的函数仍能使用;

image-20241127002604531

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;
    }
}
image-20241127004222773 image-20241127004548737 image-20241127004836011

26.ABI编码解码

ABI-(Application Binary Interface,应用二进制接口),是与以太坊智能合约交互的标准。

数据基于他们的类型编码,并且由于编码后不包含类型信息,解码时需要注明它们的类型;

编码abi.encodeabi.encodePackedabi.encodeWithSignatureabi.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
image-20241129224205133

27.选择器

27_01.calldata

当我们调用智能合约时,本质上是向目标合约发送了一段calldata,发送交易后,可以在详细信息的input中看到此次交易的calldata

image-20241129224706383

发送的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)booladdress等;

bytes(keccak256("func_name(uint256,bool,...)"));

固定长度类型参数

固定长度类型的参数,比如:uint256[3]

bytes(keccak256("func_name(uint256[3])"));

可变长度类型参数

可变长度类型的参数,比如:address[]uint[]stringbytes等;

bytes(keccak256("func_name(bytes,string)"));

映射类型参数

映射类型的参数有:contractenumstruct等;

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);
        }
    }
}

成功:

image-20241130003918089

失败:

image-20241130003955781

调用合约(合约创建失败)

// 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:

image-20241130004935488

exec(1) --> 失败,释放CatchByte:

image-20241130005020918

exec(2) --> 成功,释放SuccessEvent:

image-20241130005053655

参考:https://github.com/AmazingAng/WTF-Solidity

posted @ 2024-11-30 00:55  -WZM-  阅读(155)  评论(0)    收藏  举报