Solidity教程之基础篇

Solidity教程之基础篇
solidity

Hello World

pragma solidity ^0.8.13; 表明当前编译合约的版本号,向上兼容至0.9
```js
// SPDX-License-Identifier: MIT
// compiler version must be greater than or equal to 0.8.13 and less than 0.9.0
pragma solidity ^0.8.13;
contract HelloWorld {
    string public greet = "Hello World!";
}

Try on Remix

第一个Dapp

// 指定编译器版本,版本标识符
pragma solidity ^0.8.13;
// 关键字 contract 跟java的class一样  智能合约是Inbox      
contract Inbox{
    
    // 状态变量,存在链上
	string public message;
    
    // 构造函数
	constructor(string memory initMessage) {
        // 本地变量
        string memory tmp = initMessage;
        message = tmp;
	}
  
    // 写操作,需要支付手续费
    function setMessage(string memory _newMessage) public {
        message = _newMessage;
    }
    
    // 读操作,不需要支付手续费
    function getMessage() public view returns(string memory) {
        return message;
    }
}

Try on Remix

基础数据类型

  • int(有符号整型,有正有负)int默认为int256
  • uint(无符号整型,无负数)uint默认为uint256
  • 以8位为区间,支持int8,int16,int24 至 int256(uint同理)
  • bool类型:true,false
  • 定长字节:bytes1~bytes32
  • 地址:address(20个字节,40个16进制字符,共160位),如:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Primitives {
    bool public flag = true;
    /*
    uint stands for unsigned integer, meaning non negative integers
    different sizes are available
        uint8   ranges from 0 to 2 ** 8 - 1
        uint16  ranges from 0 to 2 ** 16 - 1
        ...
        uint256 ranges from 0 to 2 ** 256 - 1
    */
    uint8 public u8 = 1;
    uint public u256 = 456;
    uint public u = 123; // uint is an alias for uint256
    /*
    Negative numbers are allowed for int types.
    Like uint, different ranges are available from int8 to int256
    
    int256 ranges from -2 ** 255 to 2 ** 255 - 1
    int128 ranges from -2 ** 127 to 2 ** 127 - 1
    */
    int8 public i8 = -1;
    int public i256 = 456;
    int public i = -123; // int is same as int256
    // minimum and maximum of int
    int public minInt = type(int).min;
    int public maxInt = type(int).max;
    address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;
    /*
    In Solidity, the data type byte represent a sequence of bytes. 
    Solidity presents two type of bytes types :
     - fixed-sized byte arrays
     - dynamically-sized byte arrays.
     
     The term bytes in Solidity represents a dynamic array of bytes. 
     It’s a shorthand for byte[] .
    */
    bytes1 a = 0xb5; //  [10110101]
    bytes1 b = 0x56; //  [01010110]
    // Default values
    // Unassigned variables have a default value
    bool public defaultBoo; // false
    uint public defaultUint; // 0
    int public defaultInt; // 0
    address public defaultAddr; // 0x0000000000000000000000000000000000000000
}

Try on Remix

变量variables

以太坊一共有三种类型的变量

1.状态变量(state)

  • 定义在合约内,函数外
  • 存储在链上

3.本地变量(local)

  • 定义在函数内
  • 不会存储在链上

3.全局变量(global)

  • 与当前合约无关,描述整个区块链的信息(时间,块高等)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Variables {
    // State variables are stored on the blockchain.
    string public msg = "Hello";
    uint public age = 26;
    function test() public {
        // Local variables are not saved to the blockchain.
        uint i = 456;
        // Here are some global variables
        uint height = block.blocks; // Current block height
        address sender = msg.sender; // address of the caller
    }
}

Try on Remix

常量constant

  1. 常量与变量相对,需要硬编码在合约中,合约部署之后,无法改变。
  2. 常量更加节约gas,一般用大写来代表常量。
  3. 高阶用法:clone合约时,如果合约内有初始值,必须使用constant,否则clone的新合约初始值为空值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Constants {
    // coding convention to uppercase constant variables
    address public constant MY_ADDRESS = 0x194d8e0c85f7826d828E1d2E9BCe714064F4f8D7;
    uint public constant MY_UINT = 123;
}

Try on Remix

不可变量immutable

与常量类似,但是不必硬编码,可以在构造函数时传值,部署后无法改变。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Immutable {
    // coding convention to uppercase constant variables
    address public immutable MY_ADDRESS;
    uint public immutable MY_UINT;
    constructor(uint _myUint) {
        MY_ADDRESS = msg.sender;
        MY_UINT = _myUint;
    }
}

Try on Remix

读写状态变量

  1. 写状态变量(上链)是一笔交易(tx),需要矿工打包,所以需要花费资金;
  2. 读取状态变量,是从账本中获取数据,不是一笔交易,所以免费。(必须加上view)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract SimpleStorage {
    // State variable to store a number
    uint public num;
    // You need to send a transaction to write to a state variable.
    function set(uint _num) public {
        num = _num;
    }
    // You can read from a state variable without sending a transaction.
    function get() public view returns (uint) {
        return num;
    }
}

Try on Remix

ether和wei

  • 常用单位为:wei,gwei,ether
  • 不含任何后缀的默认单位是 wei
  • 1 gwei = 10^9 wei
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract EtherUnits {
    uint public oneWei = 1 wei;
    // 1 wei is equal to 1
    bool public isOneWei = 1 wei == 1;
    uint public oneEther = 1 ether;
    // 1 ether is equal to 10^18 wei
    bool public isOneEther = 1 ether == 1e18;
}

Try on Remix

gas和gasprice

Solidity教程之基础篇
gas费用

gas描述执行一笔交易时需要花费多少ether!(1 ether = 10^18wei)

交易手续费 = gas_used * gas_price,其中:

  1. gas:是数量单位,uint
  2. gas_used:表示一笔交易实际消耗的gas数量
  3. gas_price:每个gas的价格,单位是wei或gwei
  4. gas limit:表示你允许这一笔交易消耗的gas上限,用户自己设置(防止因为bug导致的损失)
  5. 如果gas_used小于gas_limit,剩余gas会返回给用户,这个值不再合约层面设置,在交易层面设置(如metamask)
  6. 如果gas_used大于gas_limit,交易失败,资金不退回
  7. block gas limit:表示一个区块能够允许的最大gas数量,由区块链网络设置
Solidity教程之基础篇
交易信息

验证:

gas_used:  197083
gas_price: 0.000000078489891145
cost = gas_used * gas_price = 197083 * 0.000000078489891145 = 0.015469023216530034,#与上图一致

Try on Remix

if else

支持常规的if , else if, else

pragma solidity ^0.8.13;
contract IfElse {
    function foo(uint x) public pure returns (uint) {
        if (x < 10) {
            return 0;
        } else if (x < 20) {
            return 1;
        } else {
            return 2;
        }
    }
    function ternary(uint _x) public pure returns (uint) {
        // if (_x < 10) {
        //     return 1;
        // }
        // return 2;
        // shorthand way to write if / else statement
        return _x < 10 ? 1 : 2;
    }
}

Try on Remix

for and while

solidity支持for, while, do while循环;

尽量不要使用没有边界的循环,因为会导致达到gas limit,进而导致交易执行失败,因此很少使用while和do while

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Loop {
    function loop() public {
        // for loop
        for (uint i = 0; i < 10; i++) {
            if (i == 3) {
                // Skip to next iteration with continue
                continue;
            }
            if (i == 5) {
                // Exit loop with break
                break;
            }
        }
        // while loop
        uint j;
        while (j < 10) {
            j++;
        }
    }
}

Try on Remix

bytes和string

byteN、bytes、string直接的关系:

Solidity教程之基础篇
byteN、bytes、string

bytes:

  • bytes是动态数组,相当于byte数组(如:byte[10])
  • 支持push方法添加
  • 可以与string相互转换
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract  Bytes {
    bytes public name;
    //1. 获取字节长度
    function getLen() public view returns(uint256) {
        return name.length;
    }
    //2. 可以不分空间,直接进行字符串赋值,会自动分配空间
    function setValue(bytes memory input) public {
        name = input;
    }
    //3. 支持push操作,在bytes最后面追加元素
    function pushData() public {
        name.push("h");
    }
}

string:

  • string 动态尺寸的UTF-8编码字符串,是特殊的可变字节数组
  • string 不支持下标索引不支持length、push方法
  • string 可以修改(需通过bytes转换)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract  String {
    string public name = "lily";   
    function setName() public {
        bytes(name)[0] = "L";   
    }
    function getLength() public view returns(uint256) {
        return bytes(name).length;
    }
}

Mapping

  • 定义:mapping(keyType => valueType) myMapping
  • key可以是任意类型,value可以是任意类型(value也可以是mapping或者数组)
  • mapping不支持迭代器
  • 不需要实例化等,定义后直接可以使用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Mapping {
    // Mapping from address to uint
    mapping(address => uint) public myMap;
    function get(address _addr) public view returns (uint) {
        // Mapping always returns a value.
        // If the value was never set, it will return the default value.
        return myMap[_addr];
    }
    function set(address _addr, uint _i) public {
        // Update the value at this address
        myMap[_addr] = _i;
    }
    function remove(address _addr) public {
        // Reset the value to the default value.
        delete myMap[_addr];
    }
}
contract NestedMapping {
    // Nested mapping (mapping from address to another mapping)
    mapping(address => mapping(uint => bool)) public nested;
    function get(address _addr1, uint _i) public view returns (bool) {
        // You can get values from a nested mapping
        // even when it is not initialized
        return nested[_addr1][_i];
    }
    function set(
        address _addr1,
        uint _i,
        bool _boo
    ) public {
        nested[_addr1][_i] = _boo;
    }
    function remove(address _addr1, uint _i) public {
        delete nested[_addr1][_i];
    }
}

Try on Remix

array数组

  1. 定长数组(编译时确定)和动态数组
  2. 下面的arr和arr2是相同的,arr2多了三个初始化的值,arr2也支持push和pop操作
  3. 2022年,数组可以直接在构造函数中赋值
  4. new uint256 语法用于对memory进行修饰,storage不需要使用new
  5. 仅状态变量数组和storage支持动态扩容:push
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Array {
    // Several ways to initialize an array
    uint[] public arr;
    uint[] public arr2 = [1, 2, 3];
    // Fixed sized array, all elements initialize to 0
    uint[10] public myFixedSizeArr;
    function get(uint i) public view returns (uint) {
        return arr[i];
    }
    // Solidity can return the entire array.
    // But this function should be avoided for
    // arrays that can grow indefinitely in length.
    function getArr() public view returns (uint[] memory) {
        return arr;
    }
    function push(uint i) public {
        // Append to array
        // This will increase the array length by 1.
        arr.push(i);
    }
    function pop() public {
        // Remove last element from array
        // This will decrease the array length by 1
        arr.pop();
    }
    function getLength() public view returns (uint) {
        return arr.length;
    }
    function remove(uint index) public {
        // Delete does not change the array length.
        // It resets the value at index to it's default value,
        // in this case 0
        delete arr[index];
    }
    function examples() pure external returns(uint[] memory) {
        // create array in memory, only fixed size can be created
        uint[] memory a = new uint[](5);
        return a;
    }
}

demo1:删除某个位置的元素,剩余元素向左移动(保留顺序)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ArrayRemoveByShifting {
    // [1, 2, 3] -- remove(1) --> [1, 3, 3] --> [1, 3]
    // [1, 2, 3, 4, 5, 6] -- remove(2) --> [1, 2, 4, 5, 6, 6] --> [1, 2, 4, 5, 6]
    // [1, 2, 3, 4, 5, 6] -- remove(0) --> [2, 3, 4, 5, 6, 6] --> [2, 3, 4, 5, 6]
    // [1] -- remove(0) --> [1] --> []
    uint[] public arr;
    function remove(uint _index) public {
        require(_index < arr.length, "index out of bound");
        for (uint i = _index; i < arr.length - 1; i++) {
            arr[i] = arr[i + 1];
        }
        arr.pop();
    }
    function test() external {
        arr = [1, 2, 3, 4, 5];
        remove(2);
        // [1, 2, 4, 5]
        assert(arr[0] == 1);
        assert(arr[1] == 2);
        assert(arr[2] == 4);
        assert(arr[3] == 5);
        assert(arr.length == 4);
        arr = [1];
        remove(0);
        // []
        assert(arr.length == 0);
    }
}

demo2:删除某位置元素,使用最后一个元素填充(不考虑顺序)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ArrayReplaceFromEnd {
    uint[] public arr;
    // Deleting an element creates a gap in the array.
    // One trick to keep the array compact is to
    // move the last element into the place to delete.
    function remove(uint index) public {
        // Move the last element into the place to delete
        arr[index] = arr[arr.length - 1];
        // Remove the last element
        arr.pop();
    }
    function test() public {
        arr = [1, 2, 3, 4];
        remove(1);
        // [1, 4, 3]
        assert(arr.length == 3);
        assert(arr[0] == 1);
        assert(arr[1] == 4);
        assert(arr[2] == 3);
        remove(2);
        // [1, 4]
        assert(arr.length == 2);
        assert(arr[0] == 1);
        assert(arr[1] == 4);
    }
}

Try on Remix

枚举Enum

枚举可以避免魔数,让程序更加易读,更好的进行状态管理,默认第一个值是:0

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Enum {
    // Enum representing shipping status
    enum Status {
        Pending,
        Shipped,
        Accepted,
        Rejected,
        Canceled
    }
    // Default value is the first element listed in
    // definition of the type, in this case "Pending"
    Status public status;
    // Returns uint
    // Pending  - 0
    // Shipped  - 1
    // Accepted - 2
    // Rejected - 3
    // Canceled - 4
    function get() public view returns (Status) {
        return status;
    }
    // Update status by passing uint into input
    function set(Status _status) public {
        status = _status;
    }
    // You can update to a specific enum like this
    function cancel() public {
        status = Status.Canceled;
    }
    // delete resets the enum to its first value, 0
    function reset() public {
        delete status;
    }
}

Try on Remix

结构体Struct

自定义结构类型,将不同的数据类型组合到一个结构中,目前支持参数传递结构体。

枚举和结构体都可以定义在另外一个文件中,进行import后使用

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Todos {
    struct Todo {
        string text;
        bool completed;
    }
    // An array of 'Todo' structs
    Todo[] public todos;
    function create(string memory _text) public {
        // 3 ways to initialize a struct
        // - calling it like a function
        todos.push(Todo(_text, false));
        // key value mapping
        todos.push(Todo({text: _text, completed: false}));
        // initialize an empty struct and then update it
        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);
    }
    // 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;
    }
}

在外面定义结构体:StructDeclaration.sol(说明并不是所有数据都必须写在合约之内的)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
struct Todo {
    string text;
    bool completed;
}

在主合约中引用:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "./StructDeclaration.sol";
contract Todos {
    // An array of 'Todo' structs
    Todo[] public todos;
}

避坑指南

  1. 传递结构体时,需要使用方括号包裹,并且里面有地址的时候,需要使用双引号包裹,否则失败
  2. 传递结构体数组时,需要在外层再次包裹方括号即可。
  3. 示例: 函数原型:
   function createOffer(Token[] memory _tokens, GeneralInfo memory _general)

结构定义:

   enum TokenType {
       ERC20,
       ERC721,
       ERC1155
   }
   struct Token {
       TokenType tokenType;
       address tokenAddr;
       uint256 tokenAmounts;
       uint256 tokenId;
       uint256 tokenPrice;
   }
   struct GeneralInfo {
       address loanToken; //token to borrow
       uint256 ltv; //8000 means 80%
       bool featuredFlag; //true, false
       uint256 loanAmount; //amounts to borrow
       uint256 interestRate; //1100 means 11%
       uint256 collateralThreshold; //8000 means 80%
       uint256 repaymentDate; //timestamp + 30days
       uint256 offerAvailable; //timestamp + 7days
   }

正确传递参数方式如下:

   tokens:     [[0, '0x749B1c911170A5aFEb68d4B278cD5405C718fc7F',1000,0,0]],
   general: ["0x749B1c911170A5aFEb68d4B278cD5405C718fc7F", 8000, false, 1000, 1100, 8000, 10000, 10000]

Try on Remix

存储位置-memory、storage、calldata

solidity中的存储位置分为三种,使用memory、storage、calldata来进行区分:

  • storage:属于状态变量,数据会存储在链上,仅适用于所有引用类型:string,bytes,数组,结构体,mapping等;
  • memory:仅存储在内存中,供函数使用,数据不上链,适用于所有类型,包括:
  • 值类型(int,bool,bytes8等)
  • 引用类型(string,bytes,数组,结构体,mapping)
  • calldata:存储函数的参数的位置,是只读的(只有calldata支持数组切片,状态变量不可以直接使用切片,需要new新数组,然后使用for循环解决)
  • 其他:Solidity 变量中 memory 、calldata 2 个表示作用非常类似,都是函数内部临时变量,它们最大的区别就是 calldata 是不可修改的,在某些只读的情况比较省 Gas.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract DataLocations {
    uint[] public arr = [1, 2, 3];
    mapping(uint => address) public map;
    struct MyStruct {
        uint foo;
    }
    mapping(uint => MyStruct) public myStructs;
    function test() public {
        // get a struct from a mapping
        MyStruct storage myStruct = myStructs[1];
        // create a struct in memory
        MyStruct memory myMemStruct = MyStruct(0);
        // call _f with state variables
        _f(arr, map, myStruct);
        //invalid convertion, failed to call
        // _f(arr, map, myMemStruct); 
        _g(arr);
        this._h(arr);
    }
    function _f(
        uint[] storage _arr,
        mapping(uint => address) storage _map,
        MyStruct storage _myStruct
    ) internal {
        // do something with storage variables
        _arr.push(100);
        _map[20] = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
        _myStruct.foo = 20;
    }
    // You can return memory variables
    function _g(uint[] memory _arr) public returns (uint[] memory) {
        // do something with memory array
        _arr[0] = 100;
    }
    function _h(uint[] calldata _arr) external {
        // do something with calldata array
        // calldata is read-only
        // _arr[2] = 200;
    }
}

Try on Remix

函数Function

  • 可以返回多个数据,使用()包裹起来即可
  • 可以对返回值命名,此时可以省略return关键字
  • 可以解构返回值,对忽略的返回值直接使用逗号留空
  • map无法作为参数或返回值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Function {
    // Functions can return multiple values.
    function returnMany()
        public
        pure
        returns (
            uint,
            bool,
            uint
        )
    {
        return (1, true, 2);
    }
    // Return values can be named.
    function named()
        public
        pure
        returns (
            uint x,
            bool b,
            uint y
        )
    {
        return (1, true, 2);
    }
    // Return values can be assigned to their name.
    // In this case the return statement can be omitted.
    function assigned()
        public
        pure
        returns (
            uint x,
            bool b,
            uint y
        )
    {
        x = 1;
        b = true;
        y = 2;
    }
    // Use destructuring assignment when calling another
    // function that returns multiple values.
    function destructuringAssignments()
        public
        pure
        returns (
            uint,
            bool,
            uint,
            uint,
            uint
        )
    {
        (uint i, bool b, uint j) = returnMany();
        // Values can be left out.
        (uint x, , uint y) = (4, 5, 6);
        return (i, b, j, x, y);
    }
    // Cannot use map for either input or output
    // Can use array for input
    function arrayInput(uint[] memory _arr) public {}
    // Can use array for output
    uint[] public arr;
    function arrayOutput() public view returns (uint[] memory) {
        return arr;
    }
}

Try on Remix

view和pure

view和pure用于修饰Getter函数(只读取数据的函数),其中:

  1. view:表示函数中不会修改状态变量,只是读取;
  2. pure:表示函数中不会使用状态变量,既不修改也不读取。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ViewAndPure {
    uint public x = 1;
    // Promise not to modify the state.
    function addToX(uint y) public view returns (uint) {
        return x + y;
    }
    // Promise not to modify or read from the state.
    function add(uint i, uint j) public pure returns (uint) {
        return i + j;
    }
}

Try on Remix

错误Error

合约中发生错误时,整个交易状态都会进行回滚,一共有三个错误处理方式,具体如下:

  1. require:一般用于参数有效性校验,最常用。消耗的gas不会退回,剩余的gas退回;
  2. revert:与require类似,适用于校验条件复杂时使用;
  3. assert:用于断言绝对不改出错的地方,注意:
  4. 一般用于程序异常处理,触发了assert意味着存在bug;
  5. 不提供错误信息;

0.8.0之前,Asset会消耗掉所有提供的gaslimit,剩余的gas也不会返回(0.8.0之后已经不会再消耗了)

其他相关:

  1. 也可以自定义error,可以节约gas
  2. 错误消息的长度会影响:
  3. gas消耗数量
  4. 单个合约的大小
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Error {
    function testRequire(uint _i) public pure {
          // 期待_i > 10,如果i <= 10,则会抛出错误
        require(_i > 10, "Input must be greater than 10");
    }
    function testRevert(uint _i) public pure {
        // 如果校验条件过于复杂,则可以使用revert
        if (_i <= 10) {
            revert("Input must be greater than 10");
        }
    }
    uint public num;
    function testAssert() public view {
        // assert用于校验不可变量,一般用于校验内部错误
          // num == 0 为true时继续向下执行
          // 不提供错误信息
        assert(num == 0);
    }
    // custom error
    error InsufficientBalance(uint balance, uint withdrawAmount);
    function testCustomError(uint _withdrawAmount) public view {
        uint bal = address(this).balance;
        if (bal < _withdrawAmount) {
            revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
        }
    }
}

另一个示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Account {
    uint public balance;
    uint public constant MAX_UINT = 2**256 - 1;
    function deposit(uint _amount) public {
        uint oldBalance = balance;
        uint newBalance = balance + _amount;
        // balance + _amount does not overflow if balance + _amount >= balance
        require(newBalance >= oldBalance, "Overflow");
        balance = newBalance;
        assert(balance >= oldBalance);
    }
    function withdraw(uint _amount) public {
        uint oldBalance = balance;
        // balance - _amount does not underflow if balance >= _amount
        require(balance >= _amount, "Underflow");
        if (balance < _amount) {
            revert("Underflow");
        }
        balance -= _amount;
        assert(balance <= oldBalance);
    }
}

Try on Remix

修饰器modifier

修饰器用于修饰函数,在函数执行前或执行后进行调用,经常用于:

  1. 权限控制
  2. 参数校验
  3. 防止重入攻击等
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract FunctionModifier {
    // We will use these variables to demonstrate how to use modifiers.
    address public owner;
    uint public x = 10;
    bool public locked;
    constructor() {
        // Set the transaction sender as the owner of the contract.
        owner = msg.sender;
    }
    // 1. Modifier to check that the caller is the owner of the contract.
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        // Underscore is a special character only used inside
        // a function modifier and it tells Solidity to
        // execute the rest of the code.
        _;
    }
    // 2. Modifiers can take inputs. This modifier checks that the
    // address passed in is not the zero address.
    modifier validAddress(address _addr) {
        require(_addr != address(0), "Not valid address");
        _;
    }
    function changeOwner(address _newOwner) public onlyOwner validAddress(_newOwner) {
        owner = _newOwner;
    }
    // Modifiers can be called before and / or after a function.
    // This modifier prevents a function from being called while
    // it is still executing.
    modifier noReentrancy() {
        require(!locked, "No reentrancy");
        locked = true;
        _;
        locked = false;
    }
    function decrement(uint i) public noReentrancy {
        x -= i;
        if (i > 1) {
            decrement(i - 1);
        }
    }
}

Try on Remix

事件Event

事件是区块链上的日志,每当用户发起操作的时候,可以发送相应的事件,常用于:

  1. 监听用户对合约的调用
  2. 便宜的存储(用合约存储更加昂贵)

通过链下程序(如:subgraph)对合约进行事件监听,可以对Event进行搜集整理,从而做好数据统计,常用方式:

  1. 合约触发后发送事件
  2. subgraph对合约事件进行监听,计算(如:统计用户数量)
  3. 前端程序直接访问subgraph的服务,获得统计数据(这避免了在合约层面统计数据的费用,并且获取速度更快)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Event {
    // Event declaration
    // Up to 3 parameters can be indexed.
    // Indexed parameters helps you filter the logs by the indexed parameter
    event Log(address indexed sender, string message);
    event AnotherLog();
    function test() public {
        emit Log(msg.sender, "Hello World!");
        emit Log(msg.sender, "Hello EVM!");
        emit AnotherLog();
    }
}

当使用indexed关键字时:

  1. 如果是值类型的,则直接进行encode
  2. 如果是非值类型,如:array,string等,则使用keccak256哈希值

参考:

  1. https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#indexed-event-encoding
  2. https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#abi-events

Try on Remix

Inheritance、virtual、override

  1. 合约之间存在继承关系,使用关键字:is
  2. 如果父合约的方法想被子合约继承,则需要使用关键字:virtual
  3. 如果子合约想覆盖父合约的方法,则需要使用关键字:override
  4. 在子合约中如果想调用父合约的方法,需要使用关键字:super
  5. 继承的顺序很重要,遵循最远继承,即后面继承的合约会覆盖前面父合约的方法
// 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();
    }
}

其中:

  1. 调用foo的时候,由于B,C中的foo都没有使用super,所以只是覆盖问题,根据最远继承,C
    覆盖了B,所以执行顺序为:D -> C -> A
  2. 调用bar的时候,由于B,C中的bar使用了super,此时D的两个parent都需要执行一遍,因此为D-> C -> B -> A

Try on Remix

继承的状态变量覆盖(shadowing)

状态变量无法进行重写,只能继承父类状态变量

但是可以通过在子合约的构造函数中进行覆盖,从而达到重写目的

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract A {
    string public name = "Contract A";
    function getName() public view returns (string memory) {
        return name;
    }
}
// Shadowing is disallowed in Solidity 0.6
// This will not compile
// contract B is A {
//     string public name = "Contract B";
// }
contract C is A {
    // This is the correct way to override inherited state variables.
    constructor() {
        name = "Contract C";
    }
    // C.getName returns "Contract C"
}

Try on Remix

构造函数constructor

构造函数是可选的,在部署合约时会自动被调用

在合约继承的时候,如果父合约有构造函数,则需要显示的对父合约进行构造

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// 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;
    }
}
// There are 2 ways to initialize parent contract with parameters.
// 1. Pass the parameters here in the inheritance list.
contract B is X("Input to X"), Y("Input to Y") {
}
contract C is X, Y {
    // 2. Pass the parameters here in the constructor,
    // similar to function modifiers.
    constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}
// Parent constructors are always called in the order of inheritance
// regardless of the order of parent contracts listed in the
// constructor of the child contract.
// 构造顺序取决于继承顺序(由左至右),而不是实例化顺序
// Order of constructors called:
// 1. X
// 2. Y
// 3. D
contract D is X, Y {
    constructor() X("X was called") Y("Y was called") {}
}
// Order of constructors called:
// 1. X
// 2. Y
// 3. E
contract E is X, Y {
    constructor() Y("Y was called") X("X was called") {}
}

Try on Remix

可见性visibility

合约的方法和状态变量需要使用关键字进行修饰,从而决定其是否可以被其他合约调用,修饰符包括:

  • public:所有的合约和外部账户(EOA)都可以调用;
  • private:只允许合约内部调用;
  • internal:仅允许合约内部以及子合约中调用;
  • external:仅允许外部合约调用,合约及子合约都不能调用;(早期版本可以使用this调用external方法)

另,状态变量可以被修饰为:publi, private, internal,但是无法修饰为external

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Base {
    // Private function can only be called
    // - inside this contract
    // Contracts that inherit this contract cannot call this function.
    function privateFunc() private pure returns (string memory) {
        return "private function called";
    }
    function testPrivateFunc() public pure returns (string memory) {
        return privateFunc();
    }
    // Internal function can be called
    // - inside this contract
    // - inside contracts that inherit this contract
    function internalFunc() internal pure returns (string memory) {
        return "internal function called";
    }
    function testInternalFunc() public pure virtual returns (string memory) {
        return internalFunc();
    }
    // Public functions can be called
    // - inside this contract
    // - inside contracts that inherit this contract
    // - by other contracts and accounts
    function publicFunc() public pure returns (string memory) {
        return "public function called";
    }
    // External functions can only be called
    // - by other contracts and accounts
    function externalFunc() external pure returns (string memory) {
        return "external function called";
    }
    // This function will not compile since we're trying to call
    // an external function here.
    // function testExternalFunc() public pure returns (string memory) {
    //     return externalFunc();
    // }
    // State variables
    string private privateVar = "my private variable";
    string internal internalVar = "my internal variable";
    string public publicVar = "my public variable";
    // State variables cannot be external so this code won't compile.
    // string external externalVar = "my external variable";
}
contract Child is Base {
    // Inherited contracts do not have access to private functions
    // and state variables.
    // function testPrivateFunc() public pure returns (string memory) {
    //     return privateFunc();
    // }
    // Internal function call be called inside child contracts.
    function testInternalFunc() public pure override returns (string memory) {
        return internalFunc();
    }
}

Try on Remix

abstract

手册:https://docs.soliditylang.org/en/v0.8.14/contracts.html?highlight=abstract#abstract-contracts

抽象合约的作用是将函数定义和具体实现分离,从而实现解耦、可拓展性,其使用规则为:

  1. 当合约中有未实现的函数时,则合约必须修饰为abstract;
  2. 当合约继承的base合约中有构造函数,但是当前合约并没有对其进行传参时,则必须修饰为abstract;
  3. abstract合约中未实现的函数必须在子合约中实现,即所有在abstract中定义的函数都必须有实现;
  4. abstract合约不能单独部署,必须被继承后才能部署;
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Animal {
    string public species;
    constructor(string memory _base) {
        species = _base;
    }
}
abstract contract Feline {
    uint public num;
    function utterance() public pure virtual returns (bytes32);
    function base(uint _num) public returns(uint, string memory) {
        num = _num;
        return (num, "hello world!");
    }
}
// 由于Animal中的构造函数没有进行初始化,所以必须修饰为abstract
abstract contract Cat1 is Feline, Animal {
    function utterance() public pure override returns (bytes32) { return "miaow"; }
}
contract Cat2 is Feline, Animal("Animal") {
    function utterance() public pure override returns (bytes32) { return "miaow"; }
}

Interface

可以使用Interface完成多个合约之间进行交互,interface有如下特性:

  1. 接口中定义的function不能存在具体实现;
  2. 接口可以继承;
  3. 所有的function必须定义为external;
  4. 接口中不能存在constructor函数;
  5. 接口中不能定义状态变量。
  6. abstract和interface的区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Counter {
    uint public count;
    function increment() external {
        count += 1;
    }
}
interface IBase {
    function count() external view returns (uint);
}
interface ICounter is IBase {
    function increment() external;
}
contract MyContract {
    function incrementCounter(address _counter) external {
        ICounter(_counter).increment();
    }
    function getCount(address _counter) external view returns (uint) {
        return ICounter(_counter).count();
    }
}

uniswap demo:

// Uniswap example
interface UniswapV2Factory {
    function getPair(address tokenA, address tokenB)
        external
        view
        returns (address pair);
}
interface UniswapV2Pair {
    function getReserves()
        external
        view
        returns (
            uint112 reserve0,
            uint112 reserve1,
            uint32 blockTimestampLast
        );
}
contract UniswapExample {
    address private factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
    address private dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address private weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    function getTokenReserves() external view returns (uint, uint) {
        address pair = UniswapV2Factory(factory).getPair(dai, weth);
        (uint reserve0, uint reserve1, ) = UniswapV2Pair(pair).getReserves();
        return (reserve0, reserve1);
    }
}

Try on Remix

library

库与合约类似,限制:不能在库中定义状态变量,不能向库地址中转入ether;

库有两种存在形式:

  1. 内嵌(embedded):当库中所有的方法都是internal时,此时会将库代码内嵌在调用合约中,不会单独部署库合约;
  2. 链接(linked):当库中含有external或public方法时,此时会单独将库合约部署,并在调用合约部署时链接link到库合约。
  3. 可以复用的代码可以编写到库中,不同的调用者可以linked到相同的库,因此会更加节约gas;
  4. 对于linked库合约,调用合约使用delegatecall进行调用,所以上下文为调用合约;
  5. 部署工具(如remix)会帮我们自动部署&链接合约库。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// 1. 只有internal方法,会内嵌到调用合约中
library SafeMath {
    function add(uint x, uint y) internal pure returns (uint) {
        uint z = x + y;
        require(z >= x, "uint overflow");
        return z;
    }
}
library Math {
    function sqrt(uint y) internal pure returns (uint z) {
        if (y > 3) {
            z = y;
            uint x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
        // else z = 0 (default value)
    }
}
contract TestSafeMath {
      // 对uint类型增加SafeMath的方法
    using SafeMath for uint;
    uint public MAX_UINT = 2**256 - 1;
      // 用法1:x.方法(y)
    function testAdd(uint x, uint y) public pure returns (uint) {
        return x.add(y);
    }
      // 用法2:库.方法(x)
    function testSquareRoot(uint x) public pure returns (uint) {
        return Math.sqrt(x);
    }
}
// 2. 存在public方法时,会单独部署库合约,并且第一个参数是状态变量类型
library Array {
      // 修改调用者状态变量的方式,第一个参数是状态变量本身
    function remove(uint[] storage arr, uint index) public {
        // Move the last element into the place to delete
        require(arr.length > 0, "Can't remove from empty array");
        arr[index] = arr[arr.length - 1];
        arr.pop();
    }
}
contract TestArray {
    using Array for uint[];
    uint[] public arr;
    function testArrayRemove() public {
        for (uint i = 0; i < 3; i++) {
            arr.push(i);
        }
        arr.remove(1);
        assert(arr.length == 2);
        assert(arr[0] == 0);
        assert(arr[1] == 2);
    }
}

Try on Remix

传递结构体

如何正确传值:

案例一:

https://ethereum.stackexchange.com/questions/72435/how-to-pass-struct-params-in-remix-ide

pragma solidity ^0.7;
pragma experimental ABIEncoderV2;
contract TestStruct {
      struct User {
        string name;
        uint256 age;
     }
    mapping (bytes32 => User) users;
    function addUsers (User [] memory _users) public {
        for (uint i = 0; i < _users.length; i++) {
           bytes32 hash = keccak256(abi.encode(_users[i].name));
           users[hash] = _users[i];
        }
    }
    function getUser (string memory username) public view returns (User memory) {
        bytes32 hash = keccak256(abi.encode(username));
        return users[hash];
    }
}

输入参数:

[["Duke", 20], ["Linda", 21]]

测试成功!

案例二:

// SPDX-License-Identifier: MIT
pragma solidity =0.8.10;
pragma abicoder v2;
contract StructTest {
    event Info(uint8 _type, address _addr);
    struct Token {
        uint8 tokenType;
        address tokenAddress;
    }
    function call(Token [] memory tokens) external {
        for (uint8 i = 0; i< tokens.length; i++) {
            (uint8 t, address addr) = (tokens[i].tokenType, tokens[i].tokenAddress);
            emit Info(t, addr);
        }
    }
}

输入参数:

[[0,0x5B38Da6a701c568545dCfcB03FcB875f56beddC4],[1,0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2]]

但是一直失败!

原因:地址需要使用双引号包裹传递,正确参数:

[[0,"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"],[1,"0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2"]]

当结构体中有数组

正确做法:

  1. 先使用storage创建空的数组对象
  2. 然后拼装对象,push到数组中

错误做法:

  1. 先new一个数组,拼装数据
  2. 然后赋值到结构体中
本教程仅供学习,如有疑问,请移步Telegram
post-qrcode
gason
版权声明:本文于2022-10-18转载自参考文章,共计27738字。
转载提示:此文章非本站原创文章,若需转载请联系原作者获得转载授权。
评论(没有评论)
验证码