solidity基础以及版本变化

参考网址

此处主要是针对链接中,增加中文理解说明,有需要可查看原文

(文中参考代码主要摘自该链接)https://solidity-by-example.org/

修饰符

初始变量修饰符 constants-immutable

  • constants 常量,硬编码,不可修改
  • immutable 不可变, 可在构造函数中赋值,之后不能修改
  • public 外部可访问,public变量会默认有个getter
  • private/default 私有,外部访问不了,子类可访问

数据位置 storage-memory-calldata

  • storage 全局变量都属于storage,存储在区块中,方法中使用该声明,会对区块信息修改
  • memory 存储在内存中,仅调用函数体内有效,可修改值
  • calldata 主要针对external类方法的参数,和memory的区别是不可以修改该变量

方法修饰符 view-pure

  • view 不会更改任何状态(会读取区块内变量)
  • pure 不会更改任何状态且不读取区块内变量

错误 require-revert-assert

  • require 执行的前置条件,如果这个不满足,无法往下执行, 且有提示语的话,会出现在浏览器和estimateGas提示
  • revert 在某个if判断,可调用revrt,功能和require类似
  • assert 断言某个条件是否为真

常用是require/revert, 可以将错误信息返出去,便于知道错误在哪
assert存在意义在哪不确定,可以使用require替代

另外针对assert的区别。 0.8.x测试没发现和require有什么区别,但是在低版本区别很大
例:0.4.x eth-usdt,可以去查看他的源码
如果一个交易gasLimit设置的100w,实际消耗大概是5w,代码最后面增加(require,revert)/ assert 导致出错

如果使用require/revert, 实际gasUsed = gasPrice * 5w,然后回退了
如果使用assert, 实际gasUsed = gasPrice * 100w, 会把gas消耗完…!

方法修饰符 public-private-internal-external

  • public 公开的,本合约也可以调用,外部也可以调用
  • private 私有的, 仅本合约可以调用
  • internal 内部的, 当前合约和子合约可以调用
  • external 公开的,当前合约自身不可调用

mapping

key=>value 格式

 mapping(address => uint) public myMap;
 mapping(address => mapping(uint => bool)) public nested;
 myMap[_addr] = _i;
 myMap[_addr];
 delete myMap[_addr];
 

数组

pragma solidity ^0.8.7;
contract Array {
    // 初始化数组的几种方式
    uint[] public arr;//可变长度数组,初始长度0
    uint[] public arr2 = [1, 2, 3];//可变长度数组,初始长度3,有对应值
    // 固定长度数组,所有元素是默认值,当前例子为 0,  这个数组不可以push/pop改变长度
    uint[10] public myFixedSizeArr;
    //通过下标获取数组元素
    function get(uint i) public view returns (uint) {
        return arr[i];
    }

    //可返回整个数组,这种方法需要避免长度很长可增长的数组
    //查询方法也受gasLimit限制,查询过多内容时会超限制
    function getArr() public view returns (uint[] memory) {
        return arr;
    }
    
     //数量很长的分页/区间查询
    function getArr1(uint256 pageNo, uint256 pageSize)public view returns(uint256[]memory list) {
        uint len = arr.length;
        uint start = pageNo * pageSize;
        if(len == 0  || start >= len){
            return new uint[](0);
        }
        uint end = start + pageSize;
        if(end > len){
            end = len;
        }
        uint arrLen = end - start;
        list = new uint[](arrLen);
        uint index;
        for(;start < end ; start ++){
            list[index++] = start;
        }
    }
    
    

    function push(uint i) public {
        //追加到数组,数组长度加1
        arr.push(i);
    }

    function pop() public {
        //移除数组的最后一个元素
        arr.pop();
    }
    //返回数组长度
    function getLength() public view returns (uint) {
        return arr.length;
    }

    function remove(uint index) public {
       //delete 操作不对修改数组长度,只是把索引位置的值重置为默认值,当前例子为0
        delete arr[index];
    }
    //如果想移除一个值,且改变数组长度,
    //可以先替换值, 在pop
    //注: 该方式会导致数组值不是原来的插入顺序
    function remove2(uint index)public{
        arr[index] = arr[arr.length-1];
        arr.pop();
    }

    function examples() external {
        // 在内存中创建数组,只能创建固定大小
        uint[] memory a = new uint[](5);
    }
}

struct 结构体

通过struct将相关数据放一起


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract Todos {
    struct Todo {
        string text;
        bool completed;
    }

    // An array of 'Todo' structs
    Todo[] public todos;//public声明, 外部可以通过  todos[index],输入index会返回整个结构体

    function create(string memory _text) public {
        // 3种初始化结构的方式
        // 像方法一样顺序传参
        todos.push(Todo(_text, false));

        // key value mapping
        todos.push(Todo({text: _text, completed: false}));

        // 初始化一个空的结构体,并针对每个字段赋值, 如果结构体中存在数组/其他结构体时,这种方式合适
        Todo memory todo;
        todo.text = _text;
        //未显视赋值的变量为类型初始值
        // todo.completed initialized to false

        todos.push(todo);
    }

    // Solidity automatically created a getter for 'todos' so
    // you don't actually need this function.
    function get(uint _index) public view returns (string memory text, bool completed) {
        Todo storage todo = todos[_index];
        return (todo.text, todo.completed);
    }
    //如果是低版本的,会要求在文件头部声明 pragma experimental ABIEncoderV2;
    function getObject(uint _index)public view returns(Todo memory){
        return todos[_index];
    }

    // update text
    function update(uint _index, string memory _text) public {
        Todo storage todo = todos[_index];
        todo.text = _text;
    }

    // update completed
    function toggleCompleted(uint _index) public {
        Todo storage todo = todos[_index];
        todo.completed = !todo.completed;
    }
}

modifier

可在函数调用前做一定处理,以及执行之后,再做一定处理

主要功能

  • 限制访问
  • 验证输入
  • 防止重入
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
       //下划线是一个特殊字符,仅在modifier中, 标志执行方法的其他代码
        _;
    }
    //判断输入的地址不是0地址
    modifier validAddress(address _addr) {
        require(_addr != address(0), "Not valid address");
        _;
    }
    //防止重入,调用函数前先把状态改了, 函数执行完后, 再把状态改回来
    modifier noReentrancy() {
        require(!locked, "No reentrancy");

        locked = true;
        _;
        locked = false;
    }

Events 事件 indexed

简单理解就是日志,便于知道区块内部执行了什么

event Transfer(address indexed from, address indexed to, uint value);

日志定义里面有个indexed修饰符, 最多允许3个参数使用该修饰符
event的数据,在transaction中logs中存在两个不同位置,
indexed 修饰的 在topic中 而其他的在data中

"logs": [
            {
                "address": "0x55d398326f99059ff775485246999027b3197955",
                "topics": [
                    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
                    "0x000000000000000000000000eb2d2f1b8c558a40207669291fda468e50c12345",
                    "0x000000000000000000000000bcdc55ce32a3d875d440c6fec0354919ab812345"
                ],
                "data": "0x00000000000000000000000000000000000000000000001b1ae4d6e2ef500000"
            }
        ]

logs中说明
address 表示该事件是哪个合约地址的(注意是实际发出的地址, 比如A合约调用B代币, address是B代币的地址,而不是A合约的地址)

topics 是个数组,topics[0] 表示事件名,后面的就是顺序取事件中的indexed修饰的参数
以下针对Transfer事件的说明

topics[0]对应的内容是 keccak256(bytes('Transfer(address,address,uint256)'))
topics[1]对应的内容是事件中的 from
topics[2]对应的内容是事件中的 to

data 是非indexed修饰的参数,顺序取

去掉前面的0x后, 每64位表示一个参数,解析的时候对应参数类型解析即可

Constructor 构造方法

继承多父类是从左到右的顺序

参考下面代码中注释, 链接中描述顺序是Y-X-child 是错的

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// Base contract X
contract X {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }
}

// Base contract Y
contract Y {
    string public text;

    constructor(string memory _text) {
        text = _text;
    }
}

// 有两种方式用参数初始化父合约。

//方法一、 在合约继承的时候填参数
contract B is X("Input to X"), Y("Input to Y") {

}
//方法二、在构造方法中传参
contract C is X, Y {
    
    constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}

// 父构造函数总是按照继承的顺序调用,而不管子合约的构造函数中列出的父合约的顺序。
// 继承是从左到右的
//下面两个的顺序都是 X,Y,child

contract D is X, Y {
    constructor() X("X was called") Y("Y was called") {}
}

contract E is X, Y {
    constructor() Y("Y was called") X("X was called") {}
}

继承 Inheritance

以下是例子
继承的顺序是从左到右;

例:E is C,B (和链接中有override(B, C)有改动)

先继承C,再继承B; C/B有同名方法,后继承B,所以B会覆盖掉C的方法实现

虽然方法中有 override(B, C) 但是无效,以合约中is的继承顺序, B在后面返回的结果是B

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

/* Graph of inheritance
    A
   / \
  B   C
 / \ /
F  D,E

*/

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

// Contracts inherit other contracts by using the keyword 'is'.
contract B is A {
    // Override A.foo()
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    // Override A.foo()
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

// Contracts can inherit from multiple parent contracts.
// When a function is called that is defined multiple times in
// different contracts, parent contracts are searched from
// right to left, and in depth-first manner.

contract D is B, C {
    // D.foo() returns "C"
    // since C is the right most parent contract with function foo()
    function foo() public pure override(B, C) returns (string memory) {
        return super.foo();
    }
}

contract E is C, B {
    // E.foo() returns "B"
    // since B is the right most parent contract with function foo()
    function foo() public pure override(B, C) returns (string memory) {
        return super.foo();
    }
}

// Inheritance must be ordered from “most base-like” to “most derived”.
// Swapping the order of A and B will throw a compilation error.
contract F is A, B {
    function foo() public pure override(A, B) returns (string memory) {
        return super.foo();
    }
}

调用父类合约

根据下面的描述以及测试结果
B,C中foo/bar的区别是 bar都是使用super.bar(),而foo都是使用A.bar()

调用D.foo, 调用了C,再调用了A
调用D.bar,调用了C,然后调用B,最后调用了A,(A只调用了一次)

两个的区别可以看出,同名方法是后继承覆盖先继承。
C中bar调用的super,最终到了B, B再调用superA

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

/* Inheritance tree
   A
 /  \
B   C
 \ /
  D
*/

contract A {
    // This is called an event. You can emit events from your function
    // and they are logged into the transaction log.
    // In our case, this will be useful for tracing function calls.
    event Log(string message);

    function foo() public virtual {
        emit Log("A.foo called");
    }

    function bar() public virtual {
        emit Log("A.bar called");
    }
}

contract B is A {
    function foo() public virtual override {
        emit Log("B.foo called");
        A.foo();
    }

    function bar() public virtual override {
        emit Log("B.bar called");
        super.bar();
    }
}

contract C is A {
    function foo() public virtual override {
        emit Log("C.foo called");
        A.foo();
    }

    function bar() public virtual override {
        emit Log("C.bar called");
        super.bar();
    }
}

contract D is B, C {
    // Try:
    // - Call D.foo and check the transaction logs.
    //   Although D inherits A, B and C, it only called C and then A.
    // - Call D.bar and check the transaction logs
    //   D called C, then B, and finally A.
    //   Although super was called twice (by B and C) it only called A once.

    function foo() public override(B, C) {
        super.foo();
    }

    function bar() public override(B, C) {
        super.bar();
    }
}

payable

声明的函数和地址加了payable才可以接收ether
如果对一个普通地址转ether,需要先用payable(address)转换成 address payable,再进行转ether

pragma solidity ^0.8.7;

contract Payable {
    // Payable address can receive Ether
    address payable public owner;

    // Payable constructor can receive Ether
    constructor() payable {
        owner = payable(msg.sender);
    }

    // Function to deposit Ether into this contract.
    // Call this function along with some Ether.
    // The balance of this contract will be automatically updated.
    function deposit() public payable {}
}
    

发送以太坊 Sending Ether (transfer, send, call)

有三种方式给地址转ether

  • transfer (2300 gas, 超出会抛异常回退)
  • send (2300 gas, 返回bool,超出会调用失败)
  • call (使用所有gas,或者设置gas,返回bool)

合约接收以太坊

接收 Ether 的合约必须至少具有以下功能之一

  • receive() external payable
  • fallback() external payable

如果msg.data为空,会调用 receive()
否则
会调用fallback()

应该使用哪个方法

如果接收地址是普通的地址,可以使用transfer最简单
如果接收地址是合约,且合约里面有功能的话,需要使用call,否则可能gas不足导致失败

文中介绍2019.12月后建议使用call和re-entrancy结合使用
OpenZeppelin-ReentrancyGuard

call 除了防止重入,也可以手动设置gas使用量,限制接收地址消耗太多gas

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ReceiveEther {
    /*
    Which function is called, fallback() or receive()?

           send Ether
               |
         msg.data is empty?
              / \
            yes  no
            /     \
receive() exists?  fallback()
         /   \
        yes   no
        /      \
    receive()   fallback()
    */

    event RecInfo(address indexed _from, uint256 _type, uint256 _value, bytes _data);

    // Function to receive Ether. msg.data must be empty
    receive() external payable {
        
        emit RecInfo(msg.sender, 1, msg.value, bytes(""));
    }

    // Fallback function is called when msg.data is not empty
    fallback() external payable {
        emit RecInfo(msg.sender, 2, msg.value,msg.data);
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract SendEther {
    function sendViaTransfer(address payable _to) public payable {
        // This function is no longer recommended for sending Ether.
        _to.transfer(msg.value);
    }

    function sendViaSend(address payable _to) public payable {
        // Send returns a boolean value indicating success or failure.
        // This function is not recommended for sending Ether.
        bool sent = _to.send(msg.value);
        require(sent, "Failed to send Ether");
    }

    function sendViaCall(address payable _to) public payable {
        // Call returns a boolean value indicating success or failure.
        // This is the current recommended method to use.
        // (bool sent, bytes memory data) = _to.call{value: msg.value,gas:66666}("");
        (bool sent, bytes memory data) = _to.call{value: msg.value, gas: 2300}("");
        
        //0.5.x
        //(bool sent, bytes memory data) = _to.call.value(msg.value).gas(2300)("");
        require(sent, "Failed to send Ether");
    }
}

fallback 回调函数,receive,function()

这是一个不带任何参数且不返回任何内容的函数
主要功能:

  • 需要声明回调函数,合约才可以直接接收ether
  • 调用的data匹配不到合约内方法时触发(参考delegatecall)

需要注意的是,如果使用transfer/send给该合约发送ether,受gas(2300)限制

低版本的实现(0.6.0前)

实现该方法即可接收

function() external payable {
}

0.6.0后的版本

分了两个动作;

  • receive() 仅msg.data为空才会执行
  • fallback() msg.data不为空,或者没有实现receive()方法时执行

如果只实现fallback()方法,就和之前的function()功能一样了

    /*
    Which function is called, fallback() or receive()?

           send Ether
               |
         msg.data is empty?
              / \
            yes  no
            /     \
receive() exists?  fallback()
         /   \
        yes   no
        /      \
    receive()   fallback()
    */

call

call是与其他合约交互的低级功能
call会返回bool
call合约的时候,如果data没有匹配到合约方法,会调用fillback函数

以下举例几种使用call调用其他合约的方式
(比如有些合约没有开源,但是区块浏览器可以看到交互记录/data,这个时候可以直接使用call data的方式调用合约)

contract Caller {
    event Response(bool success, bytes data);

    // Let's imagine that contract B does not have the source code for
    // contract A, but we do know the address of A and the function to call.
    function testCallFoo(address payable _addr) public payable {
        // You can send ether and specify a custom gas amount
        (bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
            abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
        );

        emit Response(success, data);
    }

    // Calling a function that does not exist triggers the fallback function.
    function testCallDoesNotExist(address _addr) public {
        (bool success, bytes memory data) = _addr.call(
            abi.encodeWithSignature("doesNotExist()")
        );

        emit Response(success, data);
    }

    function getCalldata(uint256 params1)public pure returns (bytes memory){
        bytes4 method = 0xbe221111;
        return abi.encodeWithSelector(method, params1);
    }
}

delegatecall

delegatecall是一个类似于call的低级函数。

当合约A执行delegatecall到合约时B,B代码被执行
执行后修改的信息是合约A里面的,且调用其他合约时,msg.sender是A,msg.value也是从A扣

function-selector

调用合约时的data,前4个字节(8位)是指定调用哪个方法名

transfer(address,uint256) 方法hash的结果是
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b

取前面8位就是 a9059cbb, 后面跟随的就是参数

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract FunctionSelector {
    /*
    "transfer(address,uint256)"
    0xa9059cbb
    "transferFrom(address,address,uint256)"
    0x23b872dd
    */
    function getSelector(string calldata _func) external pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }
}

调用其他合约 Calling Other Contract

调用合约有两种方式

  • 知道接口名,通过 Callee(contractAddress).method("");调用合约
  • 不知道接口名,知道方法id, 通过区块浏览器记录分析方法名和方法参数,构建交易,使用call调用

另外下面附上参数是结构体的形式调用方式, key/value的方式对应

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Callee {
    uint public x;
    uint public value;

    function setX(uint _x) public returns (uint) {
        x = _x;
        return x;
    }

    function setXandSendEther(uint _x) public payable returns (uint, uint) {
        x = _x;
        value = msg.value;

        return (x, value);
    }
}

contract Caller {
    function setX(Callee _callee, uint _x) public {
        uint x = _callee.setX(_x);
    }

    function setXFromAddress(address _addr, uint _x) public {
        Callee callee = Callee(_addr);
        callee.setX(_x);
    }

    function setXandSendEther(Callee _callee, uint _x) public payable {
        (uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
    }
}




         //结构体调用例子
        ISwapRouter.ExactInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: DAI,
                tokenOut: WBNB,
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        // The call to `exactInputSingle` executes the swap.
        amountOut = swapRouter.exactInputSingle(params);
    


合约中创建合约

其他合约可以使用new,或者create2的方式创建
0.8.0开始,create2 也支持使用new关键字加上指定salt来创建

后面附上new和create2提前计算合约地址的方式

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Car {
    address public owner;
    string public model;
    address public carAddr;

    constructor(address _owner, string memory _model) payable {
        owner = _owner;
        model = _model;
        carAddr = address(this);
    }
}

contract CarFactory {
    Car[] public cars;

    function create(address _owner, string memory _model) public {
        Car car = new Car(_owner, _model);
        cars.push(car);
    }

    function createAndSendEther(address _owner, string memory _model) public payable {
        Car car = (new Car){value: msg.value}(_owner, _model);
        cars.push(car);
    }

    function create2(
        address _owner,
        string memory _model,
        bytes32 _salt
    ) public {
        Car car = (new Car){salt: _salt}(_owner, _model);
        cars.push(car);
    }

    function create2AndSendEther(
        address _owner,
        string memory _model,
        bytes32 _salt
    ) public payable {
        Car car = (new Car){value: msg.value, salt: _salt}(_owner, _model);
        cars.push(car);
    }

    function getCar(uint _index)
        public
        view
        returns (
            address owner,
            string memory model,
            address carAddr,
            uint balance
        )
    {
        Car car = cars[_index];

        return (car.owner(), car.model(), car.carAddr(), address(car).balance);
    }
}

合约内提前计算地址

// SPDX-License-Identifier: MIT
pragma solidity ^0.5.17;
 contract Car {
    address public owner;
    string public model;
    address public carAddr;

    constructor() public payable {

    }
}
contract CarFactory {
    bytes32 public initCodeHash;
    //使用create2的方式提前计算地址
     function pairFor(uint256 _n) public view returns (address pair) {
        pair = address(uint(keccak256(abi.encodePacked(
                hex'ff',
                address(this),
                keccak256(abi.encodePacked(_n)),
                initCodeHash
            ))));
    }
    uint256 public curNum = 0;
    //该方式不知道如何传参数
    function createPair() public  returns (address pair)  {
        bytes memory bytecode = type(Car).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(curNum++));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
       
    }
    //使用create的方式提前计算地址
    function addressFrom(address _origin, uint _nonce) public pure returns (address _address) {
        bytes memory data;
        if (_nonce == 0x00) data = abi.encodePacked(byte(0xd6), byte(0x94), _origin, byte(0x80));
        else if (_nonce <= 0x7f) data = abi.encodePacked(byte(0xd6), byte(0x94), _origin, uint8(_nonce));
        else if (_nonce <= 0xff) data = abi.encodePacked(byte(0xd7), byte(0x94), _origin, byte(0x81), uint8(_nonce));
        else if (_nonce <= 0xffff) data = abi.encodePacked(byte(0xd8), byte(0x94), _origin, byte(0x82), uint16(_nonce));
        else if (_nonce <= 0xffffff) data = abi.encodePacked(byte(0xd9), byte(0x94), _origin, byte(0x83), uint24(_nonce));
        else data = abi.encodePacked(byte(0xda), byte(0x94), _origin, byte(0x84), uint32(_nonce));
        bytes32 hash = keccak256(data);
        assembly {
            mstore(0, hash)
            _address := mload(0)
        }
    }
}

try catch

try/catch 只允许外部调用以及合约创建时才可以捕获

下面例子还可以catch revert/require, 和assert的区别
或者统一处理

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// External contract used for try / catch examples
contract Foo {
    address public owner;

    constructor(address _owner) {
        require(_owner != address(0), "invalid address");
        assert(_owner != 0x0000000000000000000000000000000000000001);
        owner = _owner;
    }

    function myFunc(uint x) public pure returns (string memory) {
        require(x != 0, "require failed");
        return "my func was called";
    }
}

contract Bar {
    event Log(string message);
    event LogBytes(bytes data);

    Foo public foo;

    constructor() {
        // This Foo contract is used for example of try catch with external call
        foo = new Foo(msg.sender);
    }

    // Example of try / catch with external call
    // tryCatchExternalCall(0) => Log("external call failed")
    // tryCatchExternalCall(1) => Log("my func was called")
    function tryCatchExternalCall(uint _i) public {
        try foo.myFunc(_i) returns (string memory result) {
            emit Log(result);
        } catch {
            emit Log("external call failed");
        }
    }

    // Example of try / catch with contract creation
    // tryCatchNewContract(0x0000000000000000000000000000000000000000) => Log("invalid address")
    // tryCatchNewContract(0x0000000000000000000000000000000000000001) => LogBytes("")
    // tryCatchNewContract(0x0000000000000000000000000000000000000002) => Log("Foo created")
    function tryCatchNewContract(address _owner) public {
        try new Foo(_owner) returns (Foo foo) {
            // you can use variable foo here
            emit Log("Foo created");//创建成功
        } catch Error(string memory reason) {
            // catch failing revert() and require()
            emit Log(reason);//revert/require中的提示错误
        } catch (bytes memory reason) {
            // catch failing assert()
            emit LogBytes(reason);//assert的错误
        }
    }
}

Keccak256

keccak256 计算输入内容的Keccak-256哈希值
主要场景

  • hash后确认唯一性

下面例子中有说明一个hash冲突的问题

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract HashFunction {
    function hash(
        string memory _text,
        uint _num,
        address _addr
    ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_text, _num, _addr));
    }

    // Example of hash collision
    // Hash collision can occur when you pass more than one dynamic data type
    // to abi.encodePacked. In such case, you should use abi.encode instead.
    function collision(string memory _text, string memory _anotherText)
        public
        pure
        returns (bytes32)
    {
        // encodePacked(AAA, BBB) -> AAABBB
        // encodePacked(AA, ABBB) -> AAABBB
        return keccak256(abi.encodePacked(_text, _anotherText));
    }
}

contract GuessTheMagicWord {
    bytes32 public answer =
        0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f8e46a19dbc777c36fb0c00;

    // Magic word is "Solidity"
    function guess(string memory _word) public view returns (bool) {
        return keccak256(abi.encodePacked(_word)) == answer;
    }
}

encodePacked,encode后的区别

(AAA, BBB) 和 (AA, ABBB) 为例
encodePacked 的结果都是

0x414141424242

所以keccak256的结果都是一样的

encode
AAA BBB

0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003414141000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034242420000000000000000000000000000000000000000000000000000000000

展开
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000003
4141410000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
4242420000000000000000000000000000000000000000000000000000000000


AA ABBB

0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002414100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044142424200000000000000000000000000000000000000000000000000000000

展开
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000002
4141000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000004
4142424200000000000000000000000000000000000000000000000000000000

验证签名 Verifying Signature

可以在链下签名,链上延签,再执行一定的操作

签名步骤

1、所有参数进行hash
msgHash = hash(allparams...)
2、加上前缀再hash
ethPrefix = "\x19Ethereum Signed Message:\n32";
ethHash = hash(ethPrefix,msgHash);
3、使用私钥签名得到签名结果
signResult = sign(ethHash,prikey)
通过signResult得到r,s,v  传入到合约中

注意使用的时候,一般都会在合约里面维护一个nonces,参考UniswapV2ERC20中的permit

使用场景:

  • UniswapV2ERC20中的permit,验证签名通过后,进行授权,避免发送两次交易
  • 小狐狸签名后,把交易信息给到中心化后端,后端用于确认用户生成token鉴权
  • 项目方地址签名,合约内验签,验证通过后给调用者发送收益(参考链游提收益)

上面所说的链游提收益,是针对游戏中心化产生的积分,需要实际提到链上时,可参考meli提PCE, 代码没开源,应该也是这种方式

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

/* Signature Verification

How to Sign and Verify
# Signing
1. Create message to sign
2. Hash the message
3. Sign the hash (off chain, keep your private key secret)

# Verify
1. Recreate hash from the original message
2. Recover signer from signature and hash
3. Compare recovered signer to claimed signer
*/

contract VerifySignature {
    /* 1. Unlock MetaMask account
    ethereum.enable()
    */

    /* 2. Get message hash to sign
    getMessageHash(
        0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
        123,
        "coffee and donuts",
        1
    )

    hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
    */
    function getMessageHash(
        address _to,
        uint _amount,
        string memory _message,
        uint _nonce
    ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
    }

    /* 3. Sign message hash
    # using browser
    account = "copy paste account of signer here"
    ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)

    # using web3
    web3.personal.sign(hash, web3.eth.defaultAccount, console.log)

    Signature will be different for different accounts
    0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
    */
    function getEthSignedMessageHash(bytes32 _messageHash)
        public
        pure
        returns (bytes32)
    {
        /*
        Signature is produced by signing a keccak256 hash with the following format:
        "\x19Ethereum Signed Message\n" + len(msg) + msg
        */
        return
            keccak256(
                abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
            );
    }

    /* 4. Verify signature
    signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
    to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
    amount = 123
    message = "coffee and donuts"
    nonce = 1
    signature =
        0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
    */
    function verify(
        address _signer,
        address _to,
        uint _amount,
        string memory _message,
        uint _nonce,
        bytes memory signature
    ) public pure returns (bool) {
        bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
        bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);

        return recoverSigner(ethSignedMessageHash, signature) == _signer;
    }

    function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature)
        public
        pure
        returns (address)
    {
        (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);

        return ecrecover(_ethSignedMessageHash, v, r, s);
    }

    function splitSignature(bytes memory sig)
        public
        pure
        returns (
            bytes32 r,
            bytes32 s,
            uint8 v
        )
    {
        require(sig.length == 65, "invalid signature length");

        assembly {
            /*
            First 32 bytes stores the length of the signature

            add(sig, 32) = pointer of sig + 32
            effectively, skips first 32 bytes of signature

            mload(p) loads next 32 bytes starting at the memory address p into memory
            */

            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        // implicitly return (r, s, v)
    }
}

solidity版本变化

参考链接
https://learnblockchain.cn/docs/solidity/060-breaking-changes.html

0.5.0

  • sha3改用keccak256, keccak256只允许接收一个参数,使用abi.encodePacked等组合params
  • 构造函数由同名空参方法变成constructor

0.6.0

  • 仅标记virtual的接口才可以被覆盖,覆盖时需要使用新关键字override,如果多个基类同方法名时,需要像这样列出 override(Base1, Base2)
  • 不能通过修改length来修改数组长度,需要通过push(),push(value),pop的方式,或者赋值一个完整的数组
  • 使用abstract标识抽象合约,抽象合约不能使用new创建
  • 回调函数由function()拆分为fallback()和receive()
  • 新增try/catch,可对调用失败做一定处理
  • 数组切片,例如: abi.decode(msg.data[4:], (uint, uint)) 是一个对函数调用payload进行解码底层方法
  • payable(x) 把 address 转换为 address payable

0.7.0

  • call方式调用方法由x.f.gas(1000).value(1 ether)(arg1,arg2)改成 x.f{gas:1000,value:1 ether}(arg1,arg2)
  • now 不推荐使用,改用block.timestamp
  • gwei增加为关键字
  • 字符串支持ASCII字符,Unicode字符串
  • 构造函数不在需要 public修饰符,如需防止创建,可定义成abstract
  • 不允许在同一继承层次结构中具有同名同参数类型的多个事件
  • using A for B,只在当前合约有效, 以前是会继承的,现在需要使用的地方,都得声明一次

0.8.0

  • 弃用safeMath,默认加了溢出检查,如需不要检查使用 unchecked { ... } , 可以节省丢丢手续费
  • 默认支持ABIEncoderV2,不再需要声明
  • 求幂是右结合的,即表达式a**b**c被解析为a**(b**c)。在 0.8.0 之前,它被解析为(a**b)**c
  • assert 不在消耗完 完整的gas,功能和require基本一致,但是try/catch错误里面体现不一样,还有一定作用…
  • 不再允许使用uint(-1),改用type(uint).max
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值