Solidity 8.0 中阶-常见用法

修饰关键词

可视范围

修饰符可以用在修饰 函数状态变量

修饰符 内部可见 继承合约可见 外部可见
private Yes

|
|
| internal | Yes | Yes |
|
| public | Yes | Yes | Yes |
| external | | | Yes |

不可变量

**immutable**** **创建合约的时候不知道,创建用户地址(想要设置位常量)只定义一次后面就会成为常量, 作用就是给常量赋值(低Gas费)

// 可以节省 gas 费用
address public immutable owner = msg.sender;  

回退函数

:::info
fallbackreceive 同时存在的情况下, 没有发送 msg.value 数据时,优先调用 receive
:::

函数 接受 ETH 主币 接受数据
fallback yes yes
receive yes no

触发回退函数:

  • 调用合约中不存在的函数
  • 向合约中发送主币
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

/*
1. 调用函数在合约中不存在
2. 向合约中发送主币的时候
*/
contract Fallback{
    event Log(string func, address sender, uint value, bytes data);

    // 写法1: 【外部可见】【可以接受主币发送】
    fallback() external payable{
        emit Log("fallback", msg.sender, msg.value, msg.data);
    }

    // 写法2:只接受主币的回退方法 【receive 不接受 msg.data】只接受主币
    receive() external payable{
        emit Log("receive", msg.sender, msg.value, "");
    }
}

自毁合约

  • selfdestruct 自毁
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;


/*
selfdestruct
- 删除合约
- 强制发送主币到指定地址
*/

contract Kill{
    constructor() payable{}

    function kill() external {
        selfdestruct(payable(msg.sender));
    }
}

支付 ETH

payable 关键词,加上之后可以支付主币
image.png

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.0 <0.9.0;

contract VisibiltyBase {
  // 地址变量标记 payable 后可以发送主币
  address payable public owner;
  
  constructor() {
    owner = payable(msg.sender);  //  必须要给 msg.sender 也配上 payable 属性
  }
  
  // payable 标记后,函数可以接收 ETH 主币的传入
  function desposit() external payable{}
  
  // 测试函数,显示合约余额
  function getBalance() external view returns(uint){
    return address(this).balance; // 获取当前地址余额
  }
}

发送 ETH

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

/*
1. transfer (2300gas) 失败会返回 reverts (如果 gas 消耗完也会异常)
2. send (2300gas) 返回 bool
3. call 发送所有 gas ,返回 bool 和 data (data可以是合约的返回值)
*/
contract SendETH{
    constructor() payable {}
    receive() external payable {}  // 生成合约的时候创建 eth

    function sendTransfer(address payable _to) external payable {
        _to.transfer(123);
    }

    function sendSend(address payable _to) external payable {
        bool res = _to.send(123);
        require(res, "send failed!!");
    }

    function sendCall(address payable _to) external payable{
        (bool res, bytes memory data) = _to.call{value: 123}("");
        require(res, "Call failed!!");
    }
}

// 测试合约:接收主币发送的目标地址
contract EthReceiver {
    event Log(uint amount, uint gas);

    receive() external payable {
        emit Log(msg.value, gasleft());
    }
}

存储位置

返回值或者参数是数组、字符串、结构体,就必须指定存储位置

  • storage 存储在链上(状态变量)
  • memory 存在函数中局部变量
  • calldata 类似memory,但只能是参数
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;


/*
数据存储位置
存在 storage 是状态变量
存在 memory 内存是局部变量
存在 calldata 和内存相似,但只能是参数
*/ 
contract Test {
    // 结构体
    struct MyStruct {
        uint foo;
        string text;
    }

    mapping (address => MyStruct) public myStructs;
    
    // storage
    function examples() external {
        myStructs[msg.sender] = MyStruct({foo: 123, text: "bar"});
        MyStruct storage myStruct = myStructs[msg.sender]; // 可以进行读取写入操作
        myStruct.text = "foo"; // 要设置状态变量就不能用存储在内存上
    }

    // memory
    // 返回值或者参数是数组、字符串、结构体,就必须指定存储位置 
    function examples(uint[] memory y, string memory s) external returns(uint[] memory){
        uint[] memory memArr = new uint [](3);  // 定义数组并指定长度 3(局部内存变量必须是定长数组)
        return memArr;
    }

    // calldata 可以节约 gas
    function examples2(uint[] calldata y, string calldata s) external returns(uint[] memory){
        uint[] memory memArr = new uint [](3);  // 定义数组并指定长度 3(局部内存变量必须是定长数组)
        testCalldata(y);
        return memArr;
    }
    function testCalldata(uint[] calldata t) private {
        // code
    }
}

事件 Event

当前合约中有任何一个状态变量被改变,都应该汇报一个事件出来,这个事件就可以在区块链的浏览器上看到, 当然也可以被 web3 或其他程序监听到,用于记录信息

:::info
通常事件命名以大写字母开头
加上 indexed 可以顺带输出操作地址 msg.sender
:::

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

contract TinyAuction {
    event Store(uint256 num); // 事件 (定义一个商店的事件)
    event IndexedLog(address indexed sender, uint val);  // 加入索引,记录是哪个地址在操作
    event Message(address indexed _from, address indexed _to, string message);

    function store(uint256 num) public {  // 这里算上上面一个例子的装饰器
        emit Store(num);  // 向外部汇报这个变量
        emit IndexedLog(msg.sender, 789);  // 有索引的变量最多3个参数
    }

    function testMessage(address _to, string calldata mssage) external {
        emit Message(msg.sender, _to, mssage);
    }
}

函数修改器(父类)

使复用的代码简化的方法 Basic, inputs, sandwich

  • 普通父类
  • 带参数父类
  • 三明治父类
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

/* 
Basic 基本类型
inputs 带参数
sandwich 三明治(父类写好开头和结尾代码,中间留给子类)
*/
contract FunctionModifier{
    bool public paused;  // 是否暂停
    uint public count; 

    function setPause(bool _paused) external {
        paused = _paused;
    }
    // 父类 modifier
    // 两个函数运行前都要检测是否暂停
    modifier whenNotPaused(){
        require(!paused, "paused");  // 避免重复代码
        _;  // 后面代码位置
    }
    function inc() external whenNotPaused{
        count += 1;
    }
    function dec() external whenNotPaused{
        count -= 1;
    }

    // 带参数的父类
    modifier cap(uint _x){
        require(_x < 100, "x >= 100");
        _; 
    }
    function incBy(uint _x) external whenNotPaused cap(_x){  // 父类参数传入,父类会依次执行
        count += _x;
    }

    // 三明治父类
    modifier sandwich(){
        count += 10;
        _;
        count *= 2;
    }
    function foo() external sandwich{
        count += 1;
    }
}

构造函数

constructor
:::info
合约被创建之时触发一次, 用来定义一些初始化值,如下
:::

// 创建合约的时候定义x 值,并且将创建者设置位合约所有者
contract Constructor{
    address public owner;
    uint public x;

    // 通过构造函数初始化两个变量
    constructor(uint _x){
        owner = msg.sender;
        x = _x;
    }
}

继承合约

  • 可以被重写的函数要添加关键词 virtual
  • 子合约继承函数需要关键词 override
contract A{
    function foo() public pure virtual returns (string memory){
        return "A";
    }
}

contract B is A{
    function foo() public pure virtual override returns (string memory){
        return "B";
    }
}

contract C is B{
    function foo() public pure  override returns(string memory){
        return "C";
    }
}
  • 多线继承, C 继承了 A 和 B 两个,如果A合约更原始,那么A要写在前面
    调用父合约函数用 super.xx()

调用其他合约

源码调用

通过合约调用合约函数

  • 调用有参
  • 调用含返回值
  • 调用参数为主币的
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

/*
目标通过测试合约调用其他合约中的函数
*/ 
contract CallTestContract{
    // 方法1:直接参数为 合约类恶心
    // function setX(TestContract _test, uint _x) external pure{
    //     _test.setX(_x);
    // }

    // 方法2: 通过地址实例化
    function setX(address _test, uint _x) external{
        TestContract(_test).setX(_x);
    }
    function getX(address _test) external view returns(uint){
        return TestContract(_test).getX();
    }
    // 调用 payable 函数接受主币【需要传递主币】
    function setXandReceiveEther(address _test, uint _x) external payable {
    // 传递{}
        TestContract(_test).setXandReceiveEther{value: msg.value}(_x);
    }
    function getXandValue(address _test) external view returns(uint, uint){
        return TestContract(_test).getXandValue();
    }

}

// 测试合约
contract TestContract{
    uint public x;
    uint public value = 123;
    function setX(uint _x) external {
        x = _x;
    }
    function getX() external view returns(uint){
        return x;
    }
    // 传入 主币
    function setXandReceiveEther(uint _x) external payable {
        x = _x;
        value = msg.value;
    }
    // 2个返回值
    function getXandValue() external view returns(uint, uint){
        return (x, value);
    }
}

接口合约

:::info
在不知道其他合约源码的情况下无法直接调用, 就需要调用它的接口函数
:::

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

/*
假设不知道合约源码,或源码非常庞大
*/ 
contract Counter{
    uint public count;
    function inc() external{
        count += 1;
    }
    function dec()external{
        count -= 1;
    }
}
// ------- 假如我们不知道上面合约的源码 ------



// 接口通常以I开头
interface ICounter {
    function count() external view returns(uint);
    function inc() external;  // 写入方法,必须有
}
// 测试合约
contract TestContract{
    uint public count;
    function examples(address _couunter) external {
        ICounter(_couunter).inc();  // 调用后 ICounter 的状态变量才会发生变化 类似 Init
        count = ICounter(_couunter).count();
    }
}

低级Call

  • abi.encodeWithSignature() 通过签名确认函数
  • abi.encodeWithSelector() 通过选择器确认函数
    :::info
    参数是 uint 必须写为 uint256
    :::
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.0 <0.9.0;

contract TestCall {
    string public message;
    uint public x;

    event Log(string message);

    fallback() external payable{
        emit Log("fallback was called");
    }

    function foo(string memory _message, uint _x) external payable returns(bool, uint){
        message = _message;
        x = _x;
        return(true, 999);
    }
}


contract Call {
    bytes public data;
    // 不带主币发送  (注意 uint 必须写成 uint256)
    // abi.encodeWithSignature("函数名(参数1,参数2)",参数1,参数2)
    // 返回值1: bool 返回是否成功
    // 返回值2: data bytes 类型 是调用函数的所有返回值
    function callFoo(address _test) external {
        (bool success, bytes memory _data) = _test.call(abi.encodeWithSignature(
            "foo(string, uint256)", "call foo", 123
        ));
        require(success, "call callFoo error");
        data = _data;
    }

    // 带主币函数
    // call 后面加上 {value: 123, gas:5000}  123就是带上的 wei 数量
    function callFoo2(address _test) external payable {
        (bool success, bytes memory _data) = _test.call{value: 123}(abi.encodeWithSignature(
            "foo(string, uint256)", "call foo", 123
        ));
        require(success, "call callFoo2 error");
        data = _data;
    }

    // 调用不存在的函数(触发 fallback)
    function callFallback(address _test) external {
        (bool success, ) = _test.call(abi.encodeWithSignature("NoExist()"));
        require(success, "call Failed");
    }
}

委托调用

作用:做可升级合约,升级代码的时候我们只需要生成新的 C 合约,继续用B合约委托调用即可
A: 我们调用地址
B: 委托调用合约
C: 含有功能的调用目标合约

:::info
委托合约不能改变目标合约的状态变量,我们只能改变委托合约的状态变量,相当于套壳(只能使用目标合约的逻辑)且变量顺序必须相同
:::

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

/*
委托调用
A 地址 委托 B合约 去调用 C合约
C 合约视角就是 A 在操作调用它:
C 合约下 msg.sender = A
【委托调用不能改变所有的状态变量】
如果 A 合约委托B 向C发送 100wei, C是不能接受 wei (改变状态变量)只能存在 合约B 中
*/

// 目标合约
contract C {
    uint public num;
    address public sender;
    uint public value;
    function setValue(uint _num) external payable{
        num = _num;
        sender = msg.sender;
        value = msg.value;
    }
}

// 委托的合约
contract B{
    // 变量顺序必须与目标合约相同
    uint public num;
    address public sender;
    uint public value;
    function setValue(address _test, uint _num) external payable{
        // 委托调用合约
        // 方式1:使用签名进行编码(通过签名定位到合约)
        //_test.delegatecall(abi.encodeWithSignature("setValue(uint256)", _num));
        // 方式2:使用 select 编码(直接查找定位到合约)
        (bool success, bytes memory data) = _test.delegatecall(abi.encodeWithSelector(C.setValue.selector, _num));
        require(success, "delegatecall failed");
    }
}
// 结论: 委托合约不能改变目标合约的状态变量,我们只能改变委托合约的状态变量,相当于套壳(只能使用目标合约的逻辑)
// 作用: 我们一直调用委托合约,目标合约可以进行代码升级替换代码等等操作

合约部署合约

代理合约(内联汇编)

合约 A  和 合约 B, 我们要用代理合约去部署他们
● 代理合约使用
● 视频教程
尽管想升级已经部署的智能合约中的代码是不可能的,但是可以通过设计一个代理合约结构,这个结构可以让你可以通过新部署一个合约的方式,来实现升级主要的处理逻辑的目的。
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

contract TestContract1 {
    address public owner = msg.sender;
    function setOwner(address _owner) external {
        require(msg.sender == _owner, "error owner!");
        owner = _owner;
    }
}

contract TestContract2{
    address public owner = msg.sender;
    uint public value = msg.value;
    uint public x;
    uint public y;

    constructor(uint _x, uint _y) payable {  // payable 代表是可以可以发送主代币的方法
        x = _x;
        y = _y;
    }
}

// 通过代理合约部署上面2个测试合约
contract Proxy{
    event Deploy(address);  // 向外部汇报部署的合约地址

    fallback() external payable{}  // 回退代币函数 (代理合约可能收到主币)

    function deploy(bytes memory _code) external payable returns(address addr){
        // 通过写内联汇编部署合约
        assembly {
            /*
            create(v, p, n)  返回部署的合约地址
            v = 部署合约发送的ETH数量
            p = 内存中机器码起始位置
            n = 内存中机器码大小
            */
            addr := create(callvalue(), add(_code, 0x20), mload(_code))
        }
        require(addr != address(0), "deploy Error");
        emit Deploy(addr);  // 触发汇报
    }

    // 设置
    function execute(address _target, bytes memory _data) external payable{
        (bool success, ) = _target.call{value: msg.value}(_data);
        require(success, "failed");
    }
}

contract Helper{
    // 获取合约机器码 bytecode, 代理合约就可以用机器码(无参合约)进行部署
    function getBytecode1() external pure returns (bytes memory){
        bytes memory bytecode = type(TestContract1).creationCode;
        return bytecode;
    }

    // 带参数的(有构造函数)合约, 需要在机器码后面 链接打包的参数。
    function getBytecode2(uint _x, uint _y) external pure returns (bytes memory){
        bytes memory bytecode = type(TestContract2).creationCode;
        return abi.encodePacked(bytecode, abi.encode(_x, _y));
    }
    
    // 调用方法(这里调用设置管理员方法)
    function getCalldate(address _owner) external pure returns (bytes memory) {
        return abi.encodeWithSignature("setOwner(address)", _owner);
    }
}

合约工厂(New合约)

不用繁琐的内联汇编,直接 New

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

// 账户合约
contract Account{
    address public bank;
    address public owner;

    constructor(address _owner) payable {
        bank = msg.sender;
        owner = _owner;
    }
}

// 账户工厂合约(批量创建账户合约) 
contract AccountFactory{
    Account[] public accounts;

    function createAccount(address _owner) external payable{
        // 如果有 Account 合约源码直接 New, 没有的话需要 import
        Account account = new Account{value: 1}(_owner);  // 因为账户合约可 payable 所以需要{}传递主币
        accounts.push(account);
    }
}

合约库

library 库合约: 工具类, 可以将仓用的通用方法存储在库合约方便调用

  1. 命名大写字母开头
  2. 设定内部可视(不需要外部可见)
library Math{ 
    function max(uint x, uint y) internal pure returns (uint) {
        return x>=y ? x : y;
    }
}

contract test{
    function testMax(uint _x, uint _y) external pure returns(uint) {
        return Math.max(_x, _y);  // 调用库
    }
}

using 可以让约中所有 指定类型变量都可以直接 .合约库的方法

library ArrayLib{
    // 因为传入数组变量是状态变量,这里用storage
    function find(uint[] storage arr, uint x) internal view returns(uint) {
        for(uint i=0; i<arr.length; i++){
            if(arr[i]== x){
                return i;
            }
        }
        revert("not found");
    }
}

contract TestArray {
    // 查找指定下标的元素
    uint[] public arr = [1,2,3];  // 状态变量
    function testFind() external view returns (uint i){
        return ArrayLib.find(arr, 2);
    }
}

// 高级引用方法【推荐】
contract TestArray2{
    using ArrayLib for uint[];  // 让合约中所有 uint[] 类型变量都可以直接 .合约库的方法
    uint[] public arr = [1,2,3]; 
    function testFind() external view returns (uint i){
        return arr.find(2); // 直接加载方法即可
    }
}