Solidity

SolidityBasics

Foreword: The main business of my new job recently is blockchain-related. I have been researching blockchain-related things since May. Solidity is mainly used to write some smart contracts and produce sub-documents during my own learning process. , there are many incomplete or wrong documents, sorry everyone!

​ Solidity is a high-level language for smart contracts that runs on top of the Ethereum Virtual Machine (EVM). Solidity is an object-oriented high-level language for implementing smart contracts. Smart contracts are programs that govern the behavior of accounts within the Ethereum state.

​ It is influenced by C++, Python, and JavaScript. But as a decentralized contract that actually runs on the network, it has many differences, some of which are listed below:

  • The bottom layer of Ethereum is based on accounts, not UTXO, so there is a special Addresstype. Code for locating users, locating contracts, and locating contracts (the contract itself is also an account).

  • Since the language embedded framework supports payment, some keywords are provided, for example payable, it can directly support payment at the language level, and it is super simple.

  • Storage uses the blockchain on the network, and each state of the data can be permanently stored, so it is necessary to determine whether the variable uses memory or the blockchain.

  • The operating environment is on a decentralized network, which places more emphasis on the way in which contracts or functions are executed. Because a simple function call turns into code execution in a node on the network, it feels distributed.

  • The last big difference is its exception mechanism. Once an exception occurs, all executions will be rolled back. This is mainly to ensure the atomicity of contract execution and avoid data inconsistencies in the intermediate state.

    ​ You can try out code samples directly in your browser using the Remix IDE. Remix is ​​a web browser-based IDE that allows you to write, deploy, and manage Solidity smart contracts without having to install Solidity locally.

    Remix address: https://remix.ethereum.org/

solidity data type

​ Solidity is a statically typed language. Common statically typed languages ​​include C, C++, Java, etc. Static type means that each variable (local or state variable) needs to be assigned a type at compile time.

​ The Solidity data type looks very simple, but it is the most prone to loopholes (such as "overflow" and other problems). Another point to pay attention to is that Solidity's data type is very concerned about the size of the space it occupies. In addition, some basic data types of Solidity can be combined into complex data types.

Solidity data types can be divided into two broad categories:

  • Value Type
  • Reference Type

value type

Value type variables are used to represent data that can be stored in 32 bytes. They always copy the value when assigning or passing parameters.

Value types include:

  1. Boolean type (Booleans)
  2. Integers
  3. Fixed-length floating-point (Fixed point Numbers)
  4. Fixed-size byte arrays
  5. Rational and Integer Literals
  6. String Literals
  7. Hexadecimal literals
  8. Enumeration (Enums)
  9. Contract type (Contract)
  10. Function Types (Function Types0)
  11. Address type (Address)
  12. Address Literals

This article focuses on three commonly used types: integer, address, contract, and function.

For detailed introduction of each type, please refer to: https://learnblockchain.cn/docs/solidity/types.html

integer

int/ uint: Represent signed and unsigned integer variables with different digits. Support keywords uint8to uint256(unsigned, from 8 bits to 256 bits) and int8to int256, in 8increments of bits. uintand are aliases for and , intrespectively .uint256int256

operator:

operation type operator Remark
comparison operation <= , < , == , != , >=, > return boolean value
bit operation & (and), | (or), ^ (exclusive or), ~ (bit inversion)
shift operation << (shift left), >> (shift right)
Arithmetic + , - , - (unary negative operation, only for signed integers), * , / , % (remainder or modulo operation), ** (power)

tips:

For integers X, you can use type(X).minand type(X).maxto get the minimum and maximum values ​​of this type.

Notice:

Integers in Solidity have a range of values. For example, uint32the value range of type is 0to 2 ** 32-1. Starting from 0.8.0, arithmetic operations have two calculation modes: one is "wrapping" (truncated) mode or "unchecked" (not checked) mode, and the other is "checked" (checked) mode. By default, arithmetic operations are in "checked" mode, that is, overflow checks are performed, and if the result falls outside the range, the call will fall back with a failure exception. You can also unchecked { ... }switch to "unchecked" mode by

Integer operators are basically similar to Java operations, and you need to pay attention to the value range, which is prone to integer overflow problems. Another thing to note is that 0**0 in exponentiation is equal to 1.

address type

In Solidity, the address type is used to represent an account, and the address type has two forms.

  • address: save a 20-byte value (Ethereum address size)
  • address payable: Indicates the payable address, which is 20 bytes the same as address, but it has member functions transfer and send

The idea behind this distinction is that address payablean address that can accept ether is an ordinary addressone that cannot.

Type conversion:

Implicit conversions from address payableto are allowed , while conversions from to must be explicit, via .addressaddressaddress payablepayable()

// 0.6版本
address payable ap = payable(addr);
// 0.5版本
address payable ap = address(uint160(addr));

addressAllows conversion between and uint160, integer literal constants, bytes20and contract types.

Notice:

addressThe difference between and address payablewas introduced in version 0.5.0. Also starting from this version, the contract type no longer inherits from the address type,

However, if the contract has a payable fallback (payable fallback) function or receive function, the contract type can still be explicitly converted to

addressor address payable.

Comparison operation:

<=,<,==,!=,>=,>

The more common ones are == and !=

Address type members:

The address type is different from basic types such as integers, and the address type also has its own member properties and functions

name illustrate Membership
.balance(uint(256))
Returns the balance of address type address member attribute
.transfer(uint256 amount)
It is used to send the amount of wei ether to the address, throws an exception when it fails, and consumes a fixed 2300gas member function
.send(uint256 amount) returns(bool)
Send an amount of wei ether to the address, return false if it fails, and consume a fixed 2300 gas. addr.transfer(y) is equivalent to require(addr.send(y))

Note: (send is a low-level version, transfer should be used in most cases)
member function

contract type

Contract types are defined with the contract keyword, and each contract definition has its own type.

  • A contract type is similar to a class in Java. Each class is a type, and functions in the contract can be called through <specific contract type>.contract type member functions.
  • The contract can be explicitly converted to the address type, so that member variables of the address type can be used
  • Inside the contract, the this keyword can be used to indicate the current contract, which can be converted to an address type by address(this)

Contract type information:

Solidity starts from version 0.6. For contract C, the type information of the contract can be obtained through type©.

  • type©.name: get the name of the contract
  • type©.creationCode: Obtain the bytecode when creating the contract
  • type©.runtimeCode: Obtain the bytecode of the contract runtime

function type

A function in Solidity can also be a type, and it belongs to the value type, modified with the function keyword, a function can be assigned to a variable of a function type, a function can also be passed as a parameter, and it can also be called in a function returns a function.

Function types fall into two categories:

  • Internal function (internal)
  • External function (external)

Function type members:

A public or external (public/external) function type has the following member properties and methods

  • .address: returns the address of the contract where the function is located
  • .selector: Returns the ABI function selector

Function parameters:

Like Javascript, functions may require parameters as input. Unlike Javascript and C, they may return any number of parameters for output

The declaration method of function parameters is the same as that of variables, but the parameter names can be omitted for unused parameters.

Function parameters can be treated as local variables, and can also be assigned to the left of the equal sign.

External functions cannot accept multidimensional arrays as parameters.

return variable:

The function return variable is declared returnsafter the keyword in the same way as the parameter declaration.

returnOne or more values ​​can be returned using keywords. When the function needs to use multiple values, the statement return(v0,v1,...,vn) can be used. The number of parameters needs to be consistent with the declaration.

Some data types of non-internal functions cannot be returned, such as restricted types: multi-dimensional dynamic arrays, structures, etc.

reference type

Variables of value types are always assigned a complete and independent copy. And some complex types, such as arrays and structures, usually occupy more than 256 bits (32 bytes), and the cost of copying is very high. At this time, you can use the reference method, that is, point to a value through multiple variables with different names . Currently, application types include structs, arrays, and maps .

Notice:

A reference type can modify its value through multiple different names, while a variable of a value type has an independent copy each time. Therefore, application types must be treated more carefully than value types (somewhat similar to the problem of multi-threaded shared data in Java). If you use a reference type, you must clearly indicate the type of location (space) where the data is stored. Changing data location or type conversion will always make a copy automatically, while duplication within the same data location will only make a copy under certain circumstances.

Data location:

The reference type has an additional attribute to identify the storage location of the data. Therefore, when using the reference type, it must be clearly indicated which type of location (space) the data is stored in. There are three locations in EVM.

  • memory (memory): Its life cycle only exists during the function call. Local variables are stored in memory by default and cannot be used for external calls.
  • storage (storage): The location where the state variables are saved, as long as the contract exists, it will always be saved in the blockchain.
  • calldata (call data): a special data location used to store function parameters, which is an unmodifiable, non-persistent function parameter storage area.

Notice:

callable (call data) is the place where the parameter of the external function must be specified, but it can also be used for other variables. Try to use a callable as the data location if you can, as it will avoid copying and ensure the data cannot be modified. Arrays and structures of callable data locations can also be used in the return value of the function, but no space can be allocated for them.

The data location is in the assignment behavior:

  • Two assignments between storage and memory (or assignments from calldata) will create an independent copy.
  • Assignment from memory to memory only creates references, which means that changing a memory variable changes the value of all other memory variables that refer to the same data.
  • Assignments from storage to local storage variables also only assign a reference.
  • Other assignments to storage always make a copy. An example of this situation is an assignment to a state variable or a local variable member of a storage structure type, even if the local variable itself is a reference, a copy will be made
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract Tiny {
    uint[] x; // x 的数据存储位置是 storage, 位置可以忽略

    // memoryArray 的数据存储位置是 memory
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // 将整个数组拷贝到 storage 中,可行
        uint[] storage y = x;  // 分配一个指针(其中 y 的数据存储位置是 storage),可行
        y[7]; // 返回第 8 个元素,可行
        y.pop(); // 通过 y 修改 x,可行
        delete x; // 清除数组,同时修改 y,可行

        // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,
        // 但 storage 是“静态”分配的:
        // y = memoryArray;
        // 下面这一行也不可行,因为这会“重置”指针,
        // 但并没有可以让它指向的合适的存储位置。
        // delete y;

        g(x); // 调用 g 函数,同时移交对 x 的引用
        h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
    }

    function g(uint[] storage ) internal pure {}
    function h(uint[] memory) public pure {}
}

array

Arrays can be declared with a specified length, or can be resized (length) dynamically.

An array with elements of type Tand fixed length kcan be declared as T[k]and a dynamic array can be declared as T[]. For example, an uintarray (two-dimensional array) whose length is 5 and whose element type is a dynamic array should be declared as uint[][5](note that compared with other languages, the declaration position of the array length is reversed).

Array subscripts start from 0, and the order of subscripts when accessing an array is opposite to that at declaration.

For example: if there is a variable uint[][5] memory x, to access the second element of the third dynamic array, use x[2][1], to access the third dynamic array use x[2]. Likewise, if there is an Tarray of type T[5] aand T can also be an array, then a[2]will always be Tof type .

bytes和strings

bytesVariables of type and stringare special arrays. bytesSimilar byte[], but it will be "tightly packed" in the call data and memory (the elements will be stored together continuously, and will not be stored in a unit of 32 bytes). stringSame as bytes, but does not allow access by length or index. Both bytes and string can be used to express strings, use bytes for arbitrary length raw byte data, and use string for arbitrary length string (UTF-8) data.

map

The mapping type is similar in function to Java's Map and Python's Dict. It is a mapping relationship storage structure of key-value pairs. Mapping is a widely used type and often acts as a database-like role in contracts, such as mapping in token contracts to store account balances, and in game contracts it can be used to store the level of each account.

A mapped type is declared as mapping(_KeyType => _ValueType). Among them, _KeyTypecan be any basic type, that is, any built-in type, bytesand stringor contract type, enumeration type. Other user-defined types or complex types such as mapping, structure, and bytesarray stringtypes other than and cannot be used as _KeyTypethe type of . _ValueTypeCan be any type including mapped types.

structure

Solidity can use the struct keyword to define a custom type. In addition to using borrowed types as members, you can also use arrays, structures, and maps as members. You cannot declare a struct with its own struct as a member, but you can use it as a value type mapped in the struct.

contract

A Solidity contract is similar to a class in an object-oriented language, using the contract keyword to declare a contract. There are state variables for data persistence in the contract, and functions that can modify the state variables. When a function of another contract instance is called, an EVM function call is executed, which switches the execution context so that the state variables of the previous contract cannot be accessed.

visibility

Like many other languages, Solidity uses public and private keywords to control whether variables and functions can be used externally. Solidity provides four kinds of visibility to modify functions and state variables, namely: external (do not modify state variables), public, internal, and private.

Different visibility will also affect the function calling method. Solidity has two kinds of function calling:

  • Internal call: code transfer, directly use the function name to call
  • External calls: calls outside the contract (other contracts or web3jAPI), also become message calls or EVM calls, and the call form is cf()

Explanation of 4 Visibility:

  • external : We refer to functions modified by external as external functions. External functions are part of the contract interface, so we can initiate calls from other contracts or through transactions. An external function f() cannot be called internally, that is, f() cannot be used to initiate a call, only this.f() can be used to initiate a call.
  • public : We call the public modified function a public function, and the public function is also part of the contract interface, which can support both internal and external calls. For state variables of public type, Solidity will also automatically create an accessor function, which is a function with the same name as the state variable, used to obtain the value of the state variable.
  • internal : The functions and state variables declared by internal can only be called in the current contract or accessed in the inherited contract, that is to say, they can only be accessed through internal calls.
  • private : private functions and state variables are only used in the contract that currently defines them, and cannot be used by derived contracts. **Note:** All the content in the contract is visible at the chain level. Marking some functions or variables as private only prevents other contracts from accessing and modifying, but it does not prevent others from seeing related information

Location of visibility identifiers:

For variables it is after the type, for functions it is between the parameter list and the return keyword

pragma solidity  >=0.4.16 <0.9.0;

contract C {
    function f(uint a) private pure returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

Constructor

A constructor is a function declared using the constructor keyword. It is executed when the contract is created to run the contract initialization code. If there is no initialization code, it can also be omitted (at this time, the compiler will add a default constructor function constructor( ) public{}).

The constructor function can be a public function or an internal function internal. When the constructor function is internal, it means that the contract cannot be deployed and is only an abstract contract.

constant state constant

State variables can be declared as constant. The compiler does not reserve space for constants on storage, but replaces variables with the corresponding expression values ​​at compile time.

pragma solidity >=0.4.0 <0.7.0;

contract C {
	uint constant x = 32 * 22 + 8;
	string constant text = "abc";
}

If the value of the expression cannot be determined at compile time, you cannot assign a value to a variable modified by constant, such as some expressions that obtain the state on the chain: new, address(this).balance, block.number, msg.value, gasleft( ) etc. are not allowed.

However, built-in functions such as keccak256, sha256, ripemd160, ecrecover, addmod, and mulmod are allowed, because the structure of these function operations can be determined at compile time;

Constant currently only supports modifying strings and value types.

immutable immutable

The variable modified by immutable is to determine the value of the variable at the time of deployment. It will not change after it is assigned once in the constructor. This is a runtime assignment, which can remove the previous limitation that the constant does not support the use of runtime state assignment.

Immutable also does not occupy the storage space of state variables. When deployed, the value of the variable will be appended to the bytecode at runtime, so it is much cheaper than using state variables, and it also brings more security. (Ensure that this value can no longer be modified)

view view function

Functions can be declared as viewtypes, in which case they are guaranteed not to modify state.

The following statements are considered to modify state:

  1. Modify state variables
  2. generate event
  3. Create other contracts
  4. use selfdestruct
  5. Send ether by calling
  6. through any function not marked as vieworpure
  7. use low-level calls
  8. Use inline assembly with specific opcodes

Notice:

Getter methods are automatically marked as view

pure pure function

Functions can be declared as pure, indicating that the function neither reads nor modifies state. In addition to the modified state listed by view, the following operations are considered to read the state

  1. read state variables
  2. Access address(this).balance or .balance
  3. Access any member of block, tx, msg (except msg.sig and msg.data)
  4. call any function not marked pure
  5. Use inline assembly that includes certain opcodes

Accessor functions (getters)

For state variables of public type, the Solidity compiler will automatically create an accessor function for the contract, which is a function with the same name as the state variable, used to obtain the value of the state variable (no need to write additional functions to obtain the value of the variable)

Value type:

If the type of the state variable is a basic (value) type, an external view function with the same name and no parameters will be generated.

uint public data;
//会生成下面的函数
function data() external view returns ()uint {
	
}

array:

For an array whose state variable is marked as public, an accessor function with parameters will be generated. The parameter will access the subscript index of the array, that is, only a single element of the array can be accessed through the generated accessor function. If it is a multidimensional array, there will be multiple parameters.

uint[] public myArray;
//会生成下面的函数
function myArray(uint i) external view returns (uint) {
	return myArray[i];
}
//如果我们需要返回整个数组,需要额外添加函数
//返回整个数组方法
function getArray() external view returns (uint[] memory) {
	return maArray;
}

mapping:

For the mapping type whose state variable is marked as public, its processing method is the same as that of an array. The parameter is the key type and the return value type.

mapping (uint => uint) public idScore;
//会生产函数
function inScore(uint i) external returns (uint) {
	return idScore[i];
}

//一个稍微复杂一点的例子(嵌套映射)
pragma solidity ^0.4.0 <0.7.0;
contract complex{
	struct Data{
		uint a;
		bytes3 b;
		mapping (uint => uint) map;
	}
	mapping (uint => mapping(bool => Data[])) public data;
}
//data变量会生成以下函数
function data (uint arg1,bool arg2,uint arg3) external returns (uint a, bytes3 b){
	a = data[arg1][arg2][arg3].a;
	b = data[arg1][arg2][arg3].b;
}

receive receive ether function

The receive (receive) function of the contract is a special function, which means that the contract can be used to receive the transfer of Ethereum. A contract has at most one receive function, and the receive function is declared as:

receive() external payable{...}

The function name has only one receive keyword, no function keyword, no parameters and return value, and must have external visibility (external) and payable (payable). It can be virtual or overloaded. There can also be modifiers

The receive function will be executed when there is no additional data call to the contract (usually a contract transfer), for example, when called by addr.send() or addr.transfer(), the receive function of the contract will be executed.

If the receive function is not defined in the contract, but the fallback function modified by payable is defined, then the fallback function will be called when the ether transfer is performed. If neither the receive function nor the fallback function exists, the contract cannot receive Ether through the transfer transaction (the transfer transaction will throw an exception). But there is one exception. A contract that does not define a receive function can receive Ether as a recipient of a coinbas transaction (miner block reward transaction) or as a target of selfdestruct (destruction contract).

The receive function may only have 2300 gas available, (for example: when using send() or transfer), there is very little room for other operations other than the basic log output. The following operations consume 2300 gas:

  • write storage
  • create contract
  • Calling external functions that consume a lot of gas
  • send ether

fallback function (fallback function)

The fallback function is also a special function. The general Chinese name is "fallback function". A contract has at most one fallback function. The fallbakc function is declared as follows:

fallback() external payable {...}

tips:

In the old solidity0.6, the fallback function is an unnamed function (a function without a function name). If you see some functions without a name in some old contract codes, don't be surprised, it is a fallback function.

This function has no parameters, no return value, and no function keyword, and must meet external visibility.

If the contract function is called, and the contract does not implement the corresponding function, then the fallback function will be called. Or transfer money to a contract, but the contract does not implement the receive function, then the fallback function marked as payable will be called at this time.

It should be noted that when used in a contract, send (and transfer) will only provide 2300 gas to execute when transferring money to the contract. If the implementation of the receive or fallback function requires more calculations, the transfer will fail. In particular, the following operations will consume more than 2300 gas:

  • write storage
  • create contract
  • Calling external functions that consume a lot of gas
  • send ether

function modifier

Function modifiers can be used to change the behavior of a function, such as checking some preconditions before the function is executed.

The function modifier uses the keyword modifier, and the following code defines the onlyOwner function modifier. The onlyOwner function modifier defines a verification: the caller of the required function must be the creator of the contract, and the require function is used in the implementation of onlyOwner

pragma solidity >=0.5.0 <0.7.0

contract owned{
    function owned() public {
        owner = msg.sender();
    }
    address owner;

    modifier onlyOwner{
        require(
            msg.sender == owner,
            "Only owner can call this function."
        );
        _;
    }

    function transferOwner(address _newO) public onlyOwner{
        owner = _newO;
    }


}

The above uses the function modifier onlyOwner to modify transferOwner(), so that transferOwner() can only be successfully called if the creator is satisfied.

Function modifiers generally have a special symbol '' ;", and the function body modified by the modifier is inserted into the position of '';", so the expansion of transferOwner is:

function transferOwner(address _newO) public onlyOwner{
	require(
            msg.sender == owner,
            "Only owner can call this function."
        );
    owner = _newO;
    }

Modifiers have the following properties:

  • Modifiers can inherit
  • Modifiers can take parameters
  • Multiple modifiers for one function

illustrate:

A function with multiple modifiers means that a function can also be modified by multiple function modifiers, and the execution order of multiple modifiers is from left to right. In addition, the return statement displayed in the modifier or function body only jumps out of the current modifier or function body, and the entire execution logic will continue to execute after the _; defined by the previous modifier.

Function Overloading¶

A contract can have multiple functions with the same name that contain different parameters, which is called overloading. It should be noted that overloading external functions needs to ensure that the parameters are different at the ABI interface level.

The following code fails to compile:

pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(B _in) public pure returns (B out) {
        out = _in;
    }

    function f(address _in) public pure returns (address out) {
        out = _in;
    }
}

contract B {
}

When the above two f() functions are overloaded, one uses the contract type and the other uses the address type, but when the external ABI is expressed, it will be considered as the address type, so the overload cannot be realized.

function returns multiple values

Solidity has built-in support for tuples, which are a list of elements with a fixed number of different types. Using tuples can return multiple values, and can also be used to assign values ​​to multiple variables at the same time.

Events

Event (Event) is a very important interface between the contract and the outside world. When we initiate a transaction to the contract, the transaction is executed asynchronously on the chain, and the execution result cannot be known immediately. By triggering an event during the execution process, we can Notify the execution status change to the outside (requires external monitoring of event changes)

Events are eventdeclared through keywords, and event does not need to be implemented. We can think of an event as an interface used to be monitored.

Error Handling and Exceptions

Solidity handles errors a little differently than languages ​​like Java. Solidity handles errors by rolling back the state, that is, if an exception occurs when the contract is running, it will cancel the state changed by all calls (including sub-calls) of the current transaction, and return an error handling flag to the caller .

assert()

It is used to check (test) internal errors. An error indicates that there is a bug in the program.

  • assert(bool condition): If the condition is not met, it will result in an invalid opcode, undo the state change, and is mainly used to check internal errors.

require()

It is used to check whether input variables or contract state variables meet the conditions, and to verify the return value of calling an external contract.

  • require(bool condition): If the condition is not met, the state change is undone, mainly used to check for errors caused by input or external components.
  • require(bool condition, string memory message): If the condition is not met, the state change is undone, mainly used to check for errors caused by input or external components, and an error message can be provided at the same time.

revert()

Used to flag an error and resume the current call.

  • revert(): Terminates the run and undoes the state change
  • revert(string memory reason): Terminates the execution and undoes the state change. An explanatory string can be provided at the same time.

require or assert

In the EVM, the methods of handling assert and require exceptions are different, although they will all fall back to the state, the differences are as follows:

  1. The gas consumption is different. The assert type exception will consume all remaining gas, while require will not consume the remaining gas (the remaining gas will be returned to the caller)
  2. The operators are different. When an exception occurs in assert, Solidity will perform an invalid operation (invalid instruction 0xfe). When an exception of the require type occurs, Solidity will perform a rollback operation (REVERT instruction 0xfd)
  • Use require() first
    1. Used to check user input.
    2. Used to check the return value of contract calls, such as require(external.send(amount)).
    3. Used to check status, like msg.send == owner.
    4. Usually used at the beginning of a function.
    5. When you don't know which one to use, use require.
  • Use assert() first
    1. Used to check for overflow errors, such as z = x + y; assert(z >= x);
    2. Used to check for unusual conditions that should not occur.
    3. Used to check the contract state after a state change.
    4. Use assert sparingly.
    5. Usually used in the middle or end of a function.

try/catch

After Solidity 0.6, try/catch is added to catch exceptions from external calls, which gives us more flexibility when writing smart contracts. Useful in the following scenarios:

  • If a call is rolled back (revert), we don't want to terminate the execution of the transaction
  • We want to retry calls in the same transaction, store error status, do something about failed calls, etc.
pragma solidity <0.6.0;

contract OldTryCatch{
    function execute (uint256 amount) external{
        try this.onlyEvent(amount){
            ...
        } catch {
            ...    
        }
    }

    function onlyEvent (uint256 a) public {
        //code that can revert
        require(a % 2 == 0, "Ups! Reverting");
    }
}

Notice:

try/catch is only applicable to external calls, so this.onlyEvent() is called above, and the code inside try braces cannot be caught by catch.

Advanced Solidity

In the previous article, we sorted out some basic grammars of Solidity. Next, we will continue to understand the inheritance of contracts, the use of interfaces, and libraries. In addition, some ABIs that are not often used in development will be introduced. Understanding these is convenient for us to understand the contract operation and read other people's code in the future.

inherit

Inheritance is a feature of most high-level languages. Solidity also supports inheritance. The keyword used for Solidity inheritance is is(similar to extends or implement in languages ​​such as Java).

When a contract inherits from multiple contracts, only one contract is created on the blockchain. All base contract code is compiled into the created contract, but note that this does not deploy the base contract. So when we use super.f() to call the method of the base class, it is not a message call, but just a code jump. Derived contracts can access all non-private (private) members in the base contract, so internal functions and state variables can be used directly in the derived contract.

multiple inheritance

Solidity also supports multiple inheritance, that is, it can inherit from multiple base class contracts, just isfollow multiple base class contracts directly, and ,separate them with (commas).

contract Named is Owned,Mortal{
	...
}

Notice:

If there is an inheritance relationship between multiple base class contracts, then the writing order of the contracts after is is very important. The order should be that the base class contract comes first, and the derived contract comes later, otherwise it cannot be compiled.

base class constructor

When the derived contract inherits the base contract, if the constructor is implemented, the code of the base contract will be copied by the compiler to the constructor of the derived contract.

  • Constructor takes no parameters
contract A{
    uint public a;
    constructor() public{
        a = 1;
    }
}

contract B is A{
    uint public b;
    constructor() public{
        b = 2;
    }
}

When deploying B, it can be found that a is 1 and b is 2

  • The constructor has parameters
  1. Specify parameters directly in the inheritance list
contract A{
    uint public a;
    constructor(uint _a) internal{
        a = _a;
    }
}

contract B is A(1){
    uint public b;
    constructor() public{
        b = 2;
    }
}

That is, initialize the parameter passing of the constructor by means of contract B is A(1).

  1. Call the base class contract by using modifiers in the constructor of the derived contract
# 方式一:
contract B is A{
    uint public b;
    constructor() A(1) public{
        b = 2;
    }
}

# 方式二:
    constructor(uint _b) A(_b / 2) public{
        b = _b;
    }

abstract contract

If a contract has a constructor, and it is an internal function, or the contract contains functions that are not implemented, this contract will be marked as an abstract contract, using keywords, abstract contracts cannot be successfully abstractdeployed, they are usually used as base class contracts.

abstract contract A {
	uint public a;
	
	constructor(uint _a) internal {
		a = _a;
	}
}

An abstract contract can declare a pure virtual function, which does not have a function that implements the code concretely. Its function declaration ;ends with , not with {}.

pragma solidity ^0.8.0;

abstract contract A {
    function get () virtual public;
}

If a contract inherits from an abstract contract and does not implement all unimplemented functions by overriding, then it is an abstract contract itself, implying the design idea of ​​an abstract contract, which requires that any inheritance must implement its methods .

notes:

There is a slight difference between a pure virtual function and a virtual function modified with the virtual keyword: the virtual keyword only means that the function can be rewritten, and the virtual keyword can be modified on any function except private (private) visibility, no matter the function is A pure virtual function is still an ordinary function. Even a rewritten function can still be modified with the virtual keyword, indicating that the rewritten function can be rewritten again.

function rewriting

Virtual functions in a contract (functions decorated with virtual) can be rewritten in sub-contracts to change their behavior in the parent contract. Overridden functions need to be decorated with the keyword override.

pragma solidity ^0.8.0;


contract Base {
    function get () virtual public{}
}

contract Middle is Base{

}

contract Inherited is Middle{
    function get() public override{

    }
}

For multiple inheritance, if there are multiple parent contracts with the same defined function, all parent contract names must be specified after the override keyword.

pragma solidity ^0.8.0;


contract Base1 {
    function get () virtual public{}
}

contract Base2 {
    function get () virtual public{}
}

contract Middle is Base1, Base2{
    function get() public override( Base1, Base2){

    }
}

If the function is not marked as virtual (except for the interface that will be mentioned below, because all functions in the interface will be automatically marked as virtual), then the derived contract cannot be rewritten to change the behavior of the function. In addition, private functions cannot be marked as virtual.

If the parameters and return value of the getter function are consistent with the external function, the external function can be rewritten by the public state variable. But public state variables cannot be overridden.

pragma solidity ^0.8.0;


contract A {
    function f () external pure virtual returns(uint){
        return 5;
    } 
}

contract B is A {
    uint public override f; 
}

interface

An interface is similar to an abstract contract. The difference is that an interface does not implement any functions and has the following restrictions:

  1. Cannot inherit other contracts or interfaces
  2. cannot define constructor
  3. cannot define variable
  4. Cannot define structure
  5. cannot define enum
pragma solidity >=0.5.0 <0.7.0;

interface IToken{
	function transfer (address recipient, uint amount) external;
}

Just like extending other contracts, contracts can inherit interfaces, and the functions in the interface will be implicitly marked as virtual, which means that they will be overridden.

Interface communication between contracts:

In addition to abstract functions, interfaces are widely used in communication between contracts, that is, one contract calls the interface of another contract;

For example: The following SimpleToken contract implements the above IToken interface:

contract SimpleToken is IToken{
	function transfer (address recipient, uint256 amount) public override{
		//do something~
	}
}

Another reward contract (Award) sends bonuses to users through the SimpleToken contract. The bonus is the token represented by the SimpleToken contract. At this time, Award needs to communicate with SimpleToken (external function call).

contract Award{
	IToken immutable token;
	//部署时传入SimpleToken合约地址
	constrcutor (IToken t) public {
		token = t
	}
	//sendBonus函数用来发送奖金,通过接口函数调用SimpleToken实现转帐
	function sendBonus(address user,uint256 amount) public {
		token.transfer(user,amount);
	}
}

library

When developing a contract, there will always be some functions that are often called by multiple contracts. At this time, these functions can be packaged as a library, and the keywords of the library libraryare used to define.

pragma solidity >=0.5.0 <0.7.0;
library SafeMath{
	function add (uint a,uint b) internal pure returns (uint){
		uint c = a + b;
		requier(c > a, "SafeMath: addition overflow");
		return c;
	}
}

The SafeMath library implements an addition function add(), which can be reused in multiple contracts;

import "./SafeMath.sol";
constract addTest{
	function add (uint x,uint y) public pure returns (uint){
		return SafeMath.add(x,y);
	}
}

Of course, we can encapsulate more functions in the library, and the library is a good means of code reuse. Also note that a library is just made up of functions, it has no state of its own. When the library is in use, depending on the scenario, one is deployed in a contract embedded in the reference (it can be called an "embedded library"), and the other is deployed separately (it can be called a "link library").

Embedded library

If the library functions referenced by the contract are all internal functions, the compiler will embed the code of the library functions into the contract when compiling the contract, just like the contract itself implements these functions, and will not be deployed separately at this time. This is the case when the Add Test contract above references the SafeMath library.

link library

If there are public (public) or external (external) functions in the library code, the library will be deployed separately and have its own address on the Ethereum chain. At this time, the contract reference library is "linked" through this address (deployment contract , a link is required). The low-level function delegate calls delegatescall(). When the contract calls the function library, it uses the method of delegate call (this is the bottom-level processing method, and it does not need to be changed when writing code).

As mentioned earlier, a library has no state of its own. Because in the way of entrusting the call, the library contract function is executed in the context of the initiating contract (also known as the "calling contract", that is, the calling contract), so the variables used in the library contract function (if any) ) are variables from the calling contract, and the library contract function uses this which is also the address of the calling contract. We can also understand from another perspective that the library is deployed separately and will be referenced by multiple contracts (this is also the main function of the library: to avoid repeated deployment in multiple contracts to save gas), if the library has its own state, it must be modified by multiple calling contracts, and the certainty of the output results of calling library functions cannot be guaranteed.

We modify the add function of the previous SafeMath library to an external function:

pragma solidity >=0.5.0 <0.7.0;
library SafeMath{
	function add (uint a,uint b) external pure returns (uint){
		uint c = a + b;
		requier(c > a, "SafeMath: addition overflow");
		return c;
	}
}

The AddTest code does not need to be modified, because the SafeMath library contract is deployed independently. To call the SafeMath library, the AddTest contract must first know the address of the latter. This is equivalent to the AddTest contract will depend on the SafeMath library, so when deploying AddTest will There is a difference, there is an additional step of linking the AddTest contract with the SafeMath library:

# 下面列举了truffle的部署方法
deployer.deploy(SafrMath);
deployer.link(SafeMath,AddTest);
deployer.deploy(AddTest);

Using for

In addition to using the above SafeMath.add(x,y) method to call library functions, there is another way to use using LibA for B. It means that all library functions of LibA are associated with type B , so that library functions can be directly called in type B.

contract testLib{
	using SafeMath for uint;
	function add (uint x,uint y) public pure returns (uint){
		return x.add(y);
	}
}

using LibA for * means that the functions in LibA can be associated with any type. Using using ... for ... is like extending the capabilities of a type.

Application Binary Interface (ABI)

In the Ethereum ecosystem, the Application Binary Interface (ABI) is a standard way to interact with contracts from outside the blockchain, and between contracts.

The difference between Ethereum and Bitcoin transactions is that there is an additional data field in Ethereum transactions. The content of data will be parsed as a message call to the function. The content of data is actually the ABI code.

function selector

When a function is called, the function selector of the first four bytes specifies the function to be called, and the function selector is the first 4 bytes of a function signature Keccak (SHA-3) hash , namely:

bytes4(keccak256("count()"));

The function signature is a string containing the function name and parameter types. For example, count() above is the function signature. When the function has parameters, the basic type of the parameter is used, and the variable name is not required, so the signature of the function add(uint i) is add(uint256). If there are multiple parameters, use them to separate them, and remove the in the ,expression all spaces.

For example: the function signature of foo(uint a, bool b) is foo(uint256, bool), and the function selector calculation is

bytes4(keccak256("foo(uint256,bool)"))

Public or external (public/external) functions have a member attribute .selector to get the function selector of the function.

parameter encoding

If the function has parameters, the first 5 bytes of the encoding are the parameters of the function.

// contracts/MultiSend.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Counter{
    uint counter;

    constructor() {
        counter = 0;
    }

    function count () public {
        counter += 1;
    }

    function add (uint i) public {
        counter += i;
    }

    function get () public view returns (uint){
        return counter;
    }
}

After deploying the contract, initiate an add(16) transaction; copy the input parameters

insert image description here

 input:0x1003e2d20000000000000000000000000000000000000000000000000000000000000010
 //参数说明:
 //0x1003e2d2为add(uint256)的函数选择器(bytes4(keccak256("add(uint256)")))
 //0000000000000000000000000000000000000000000000000000000000000010为16的二进制表示(会补充到  //32个字节的长度)

Usually developers do not need to perform ABI encoding to call functions, but only need to provide the ABI interface description JSON file.

ABI interface description

The ABI interface description is a JSON file that describes all interfaces and events of the contract generated by the compiler after compiling the code.

The JSON describing the function contains the following fields:

  • type: The possible values ​​are function, constructor, fallback, and the default is function.
  • name: function name
  • inputs: A series of objects, each object contains the following properties.
    • name: parameter name.
    • type: The canonical type of the parameter.
    • components: When type is a tuple, components lists the name (name) and type (type) of each element in the tuple.
  • outputs: A series of objects similar to inputs, which can be omitted when there is no return value.
  • payable: true means that the function can receive ether, otherwise it means that it cannot be accepted, and the default value is false.
  • stateMutability: Mutability state of the function, possible values ​​are: pure, view, nonpayable, payable.
  • constant: true if the function is specified as pure or view.

The JSON describing the event contains the following fields:

  • type: always "event".
  • name: event name.
  • inputs: an array of objects, each array object contains the following properties.
    • name: parameter name.
    • type: The authoritative type of the parameter.
    • components: for tuple (tuple) type.
  • indexed: true if this field is a topic of the log, false otherwise.
  • anonymous: true if the event is declared as anonymous.

Solidity Global API

  • .balance: Get the balance of the address.
  • .transfer(): Transfer money to an address.
  • require(): Used to check whether input variables or contract state variables meet the conditions, and verify the return value of calling an external contract.
  • asset(): It is used to check (test) internal errors, and an error indicates that there is a bug in the program.
  • revert(): Used to flag errors and restore the current call.

Block and Transaction Attributes API

  • blockhash(uint blockNumber) returns (bytes32): Obtain the block hash of the specified block, the parameter blockNumber only supports passing in the latest 256 blocks, and does not include the current block (note: the return after returns means the function returns type, the same below).
  • block.coinbase(address): Obtain the address of the miner who dug out the current block (Note: () indicates the type of attribute obtained, the same below)
  • block.difficulty(uint): Get the current block difficulty.
  • block.gaslimit(uint): Get the maximum gas limit of the current block.
  • block.number(uint): Get the current block number.
  • block.timestamp(uint): Get the timestamp of the current block in seconds.
  • gasleft() returns (uint256): Get how much gas is left in the current execution.
  • msg.data(bytes): Get the complete calldata parameter data of the current call.
  • msg.sender(address): The message sender of the current call.
  • msg.sig (bytes4): The identifier of the currently called function.
  • msg.value(uint): The amount of ether sent by the current call (in wei).
  • tx.gasprice(uint): Get the gas price of the current transaction.
  • tx.origin(address payable): Get the originator of the transaction. If there is only one current call in the transaction, tx.origin will be equal to msg.sender. If multiple sub-calls are triggered in the transaction, msg.sender will be every tx.origin is still the signer who initiated the transaction.

ABI encoding and decoding function API

  • abi.decode(bytes memory encodeData, (…)) returns (…): ABI decodes the given data, and the type of the data is given in the second parameter in parentheses. For example: (uint a, uint[2] memory b, bytes memory c ) = abi.decode(data,(uint,uint[2],bytes)) is to decode 3 variables a, b, c from data data .
  • abi.encode(…) returns (bytes): ABI-encodes the given parameter, which is the reverse operation of the previous method.
  • abi.encodePacked(…) returns (bytes): Perform ABI encoding for the given parameters. Unlike the previous function encoding, which will fill the parameters to 32 bytes in length, the encoded parameter data of encodePacked will be tightly packed together.
  • abi.encodeWithSelector(bytes4 selector,…) returns (bytes): ABI encoding starts from the second parameter, and returns together with the given function selector (parameter) in front.
  • abi.encodeWithSignature(string signature,…) returns (bytes)等价于abi.encodeWithSelector(bytes4(keccak256(signture)),…) returns (bytes)。

Mathematical and Cryptographic Functions API

  • addmod(uint x, uint y, uint k) returns (uint): Calculate (x + y)% k, that is, sum first and then modulo. The summation can be performed with arbitrary precision, that is, the result of the summation can exceed the maximum value of uint (2 to the 256th power). The modulo operation checks that k != 0.
  • mulmod(uint x, uint y, uint k) returns (uint): Calculate (x * y)% k, that is, do multiplication first and then find the modulus, multiplication can be performed at any precision, that is, the result of multiplication can exceed the maximum value of uint value. The modulo operation checks that k != 0.
  • keccak256(bytes memory) returns (bytes32): Use the keccak-256 algorithm to calculate the hash.
  • sha256(bytes memory) returns (bytes32): Computes the SHA-256 hash of the parameter.
  • ripemd160(bytes memory) returns(bytes20): Computes the RIPEMD-160 hash of the parameters.
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address): use the elliptic curve signature to recover the address related to the public key (that is, obtain the address through the signed data), and return zero value on error. The function parameters correspond to the values ​​of the ECDSA signature:
    • r = 32 bytes before signature
    • s = The second 32 bytes of the signature
      s)) decodes 3 variables a, b, and c from the data.
  • abi.encode(…) returns (bytes): ABI-encodes the given parameter, which is the reverse operation of the previous method.
  • abi.encodePacked(…) returns (bytes): Perform ABI encoding for the given parameters. Unlike the previous function encoding, which will fill the parameters to 32 bytes in length, the encoded parameter data of encodePacked will be tightly packed together.
  • abi.encodeWithSelector(bytes4 selector,…) returns (bytes): ABI encoding starts from the second parameter, and returns together with the given function selector (parameter) in front.
  • abi.encodeWithSignature(string signature,…) returns (bytes)等价于abi.encodeWithSelector(bytes4(keccak256(signture)),…) returns (bytes)。

Mathematical and Cryptographic Functions API

  • addmod(uint x, uint y, uint k) returns (uint): Calculate (x + y)% k, that is, sum first and then modulo. The summation can be performed with arbitrary precision, that is, the result of the summation can exceed the maximum value of uint (2 to the 256th power). The modulo operation checks that k != 0.
  • mulmod(uint x, uint y, uint k) returns (uint): Calculate (x * y)% k, that is, do multiplication first and then find the modulus, multiplication can be performed at any precision, that is, the result of multiplication can exceed the maximum value of uint value. The modulo operation checks that k != 0.
  • keccak256(bytes memory) returns (bytes32): Use the keccak-256 algorithm to calculate the hash.
  • sha256(bytes memory) returns (bytes32): Computes the SHA-256 hash of the parameter.
  • ripemd160(bytes memory) returns(bytes20): Computes the RIPEMD-160 hash of the parameters.
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address): use the elliptic curve signature to recover the address related to the public key (that is, obtain the address through the signed data), and return zero value on error. The function parameters correspond to the values ​​of the ECDSA signature:
    • r = 32 bytes before signature
    • s = the second 32 bytes of the signature
    • v = the last byte of the signature

Guess you like

Origin blog.csdn.net/weixin_45340300/article/details/125570404