Solidity is a statically typed language, which means that the type of each variable (state and local) needs to be specified.

All identifiers (contract names, function names and variable names) are restricted to the ASCII character set.

  • It is possible to store UTF-8 encoded data in string variables.

Solidity follows a versioning model called semantic versioning, which specifies version numbers structured as three numbers separated by dots: MAJOR.MINOR.PATCH.

  • The “major” number is incremented for major and backward-incompatible changes.
  • The “minor” number is incremented as backward-compatible features are added in between major releases.
  • The “patch” number is incremented for backward-compatible bug fixes.
  • The rules for major version 0, which is for initial development of a project, are different: anything may change at any time.
    • In practice, Solidity treats the “minor” number as if it were the major version and the “patch” number as if it were the minor version. Therefore, in 0.8.17, 8 is considered to be the major version and 17 the minor version.

Solidity offers a compiler directive known as a version pragma that instructs the compiler that the program expects a specific compiler (and language) version.

  • Pragma directives are not compiled into EVM bytecode. They are only used by the compiler to check compatibility.

Variables

State variables are variables whose values are permanently stored in contract storage.

State variables can be declared as constant or immutable.

  • The value of constant variables has to be a constant at compile time and it has to be assigned where the variable is declared.
    • Any expression that accesses storage, blockchain data (e.g. block.timestamp, address(this).balance or block.number) or execution data (msg.value or gasleft()) or makes calls to external contracts is disallowed.
    • Expressions that might have a side-effect on memory allocation are allowed, but those that might have a side-effect on other memory objects are not.
      • The reason behind allowing side-effects on the memory allocator is that it should be possible to construct complex objects like e.g. lookup-tables. This feature is not yet fully usable.
    • The built-in functions keccak256, sha256, ripemd160, ecrecover, addmod and mulmod are allowed, even though, with the exception of keccak256, they do call external contracts.
  • immutable variables can be assigned an arbitrary value in the constructor of the contract or at the point of their declaration.
    • Immutables that are assigned at their declaration are only considered initialized once the constructor of the contract is executing.
      • This means you cannot initialize immutables inline with a value that depends on another immutable.
        • You can do this inside the constructor of the contract.
      • This is a safeguard against different interpretations about the order of state variable initialization and constructor execution, especially with regards to inheritance.
    • They can be assigned only once and can, from that point on, be read even during construction time.
    • The contract creation code (actually stored in the blockchain) generated by the compiler will modify the contract’s runtime code before it is returned by replacing all references to immutables with the values assigned to them.
      • This is important if you are comparing the runtime code generated by the compiler with the one actually stored in the blockchain.
  • In both cases, the variables cannot be modified after the contract has been constructed.
  • The compiler does not reserve a storage slot for these variables, and every occurrence is replaced by the respective value.
  • Compared to regular state variables, the gas costs of constant and immutable variables are much lower.
    • For a constant variable, the expression assigned to it is copied to all the places where it is accessed and also re-evaluated each time.
      • This allows for local optimizations.
    • immutable variables are evaluated once at construction time and their value is copied to all the places in the code where they are accessed.
      • For these values, 32 bytes are reserved, even if they would fit in fewer bytes.
        • Due to this, constant values can sometimes be cheaper than immutable values.
  • For constants and immutables, only strings and value types are implemented.

State Variable Visibility:

  • public: Public state variables differ from internal ones only in that the compiler automatically generates getter functions for them, which allows other contracts (and DApps) to read their values.
    • When used within the same contract, the external access (e.g. this.x) invokes the getter while internal access (e.g. x) gets the variable value directly from storage.
    • Setter functions are not generated so other contracts cannot directly modify their values.
  • internal: The default visibility; Internal state variables can only be accessed from within the contract they are defined in and in derived contracts.
  • private: Private state variables are like internal ones but they are not visible in derived contracts.

A variable which is declared will have an initial default value whose byte-representation is all zeros. The “default values” of variables are the typical “zero-state” of whatever the type is.

  • The default value for a bool is false.
  • The default value for the uint or int types is 0.
  • For statically-sized arrays and bytes1 to bytes32, each individual element will be initialized to the default value corresponding to its type.
  • For dynamically-sized arrays, bytes and string, the default value is an empty array or string.
  • For the enum type, the default value is its first member.

Data Types

Value Types

Variables of value types will always be passed by value, i.e. they are always copied when they are used as function arguments or in assignments.

Integer (int, uint):

  • Signed (int) and unsigned (uint) integers, declared in increments of 8 bits from int8 to uint256.
    • Without a size suffix, 256-bit quantities are used, to match the word size of the EVM.
  • For an integer type X, you can use type(X).min and type(X).max to access the minimum and maximum value representable by the type.
  • Integers in Solidity are restricted to a certain range. There are two modes in which arithmetic is performed on these types: The “wrapping” or “unchecked” mode and the “checked” mode.
    • By default, arithmetic is always “checked”, which mean that if the result of an operation falls outside the value range of the type, the call is reverted through a failing assertion.
    • Switch to “unchecked” mode using unchecked { ... }.
  • Operators:
    • The result of a shift operation has the type of the left operand, truncating the result to match the type.
      • The result is always truncated. Overflow checks are never performed for shift operations as they are done for arithmetic operations.
      • The right operand must be of unsigned type, trying to shift by a signed type will produce a compilation error.
    • Division:
      • Since the type of the result of an operation is always the type of one of the operands, division on integers always results in an integer.
      • In Solidity, division rounds towards zero (int256(-5) / int256(2) == int256(-2)).
      • Division by zero causes a Panic error. This check can not be disabled through unchecked { ... }.
      • The expression type(int).min / (-1) is the only case where division causes an overflow.
        • In checked arithmetic mode, this will cause a failing assertion.
        • In wrapping mode, the value will be type(int).min.
    • Modulo:
      • a % n yields the remainder r after the division of the operand a by the operand n, where q = int(a / n) and r = a - (n * q).
        • Deduced that modulo results in the same sign as its left operand (or zero)
      • Modulo with zero causes a Panic error. This check can not be disabled through unchecked { ... }.
    • Exponentiation:
      • The resulting type of an exponentiation is always equal to the type of the base.
      • Exponentiation is only available for unsigned types in the exponent.
      • 0**0 is defined by the EVM as 1.
      • In checked mode, exponentiation only uses the comparatively cheap exp opcode for small bases.
        • For the cases of x**3, the expression x*x*x might be cheaper.
        • In any case, gas cost tests and the use of the optimizer are advisable.

Fixed-size byte arrays:

  • The value types bytes1, bytes2, bytes3, …, bytes32 hold a sequence of bytes from 1 to up to 32.

Rational and Integer Literals:

  • Integer literals are formed from a sequence of digits in the range 0-9.

    • They are interpreted as decimals.
    • Octal literals do not exist in Solidity and leading zeros are invalid.
  • Decimal fractional literals are formed by a . with at least one number after the decimal point.

    • Examples include .1 and 1.3 (but not 1.).
  • Scientific notation in the form of MeE is supported.

    • The mantissa can be fractional but the exponent has to be an integer.
    • MeE is equivalent to M * 10**E.
    • Examples include 2e10, -2e10, 2e-10, 2.5e1.
  • Underscores can be used to separate the digits of a numeric literal to aid readability.

    • Underscores are only allowed between two digits and only one consecutive underscore is allowed.
    • Decimal 123_000, hexadecimal 0x2eff_abde, scientific decimal notation 1_2e345_678 are all valid.
    • There is no additional semantic meaning added to a number literal containing underscores, the underscores are ignored.
  • Number literal expressions retain arbitrary precision until they are converted to a non-literal type (i.e. by using them together with anything other than a number literal expression (like boolean literals) or by explicit conversion).

    • This means that computations do not overflow and divisions do not truncate in number literal expressions.

      • (2**800 + 1) - 2**800 results in the constant 1 (of type uint8) although intermediate results would not even fit the machine word size.
      • .5 * 8 results in the integer 4 (although non-integers were used in between).
    • Number literal expressions are converted into a non-literal type as soon as they are used with non-literal expressions.

      uint128 a = 1;
      uint128 b = 2.5 + a + 0.5;
      
      • Because a is of type uint128, the expression 2.5 + a has to have a proper type. Since there is no common type for the type of 2.5 and uint128, the Solidity compiler does not accept this code.
    • While most operators produce a literal expression when applied to literals, there are certain operators that do not follow this pattern: ternary operator, array subscript.

      • Expressions like 255 + (true ? 1 : 0) or 255 + [1, 2, 3][0] are computed within the type uint8 and can overflow.
  • Decimal and hexadecimal number literals can be implicitly converted to any integer type that is large enough to represent it without truncation.

    uint8 a = 12;        // fine
    uint32 b = 1234;     // fine
    uint16 c = 0x123456; // fails, since it would have to truncate to 0x3456 
    
  • Decimal number literals cannot be implicitly converted to fixed-size byte arrays; hexadecimal number literals can be, but only if the number of hex digits exactly fits the size of the bytes type.

    bytes2 a = 54321;   // not allowed
    bytes2 b = 0x12;    // not allowed
    bytes2 c = 0x123;   // not allowed
    bytes2 d = 0x1234;  // fine
    bytes2 e = 0x0012;  // fine
    
    bytes4 f = 0;       // fine
    bytes4 g = 0x0;     // fine
    
    • As an exception both decimal and hexadecimal literals which have a value of zero can be converted to any fixed-size bytes type.
  • Any operator that can be applied to integers can also be applied to number literal expressions as long as the operands are integers.

    • If any of the two is fractional, bit operations are disallowed and exponentiation is disallowed if the exponent is fractional (because that might result in a non-rational number).
    • Shifts and exponentiation with literal numbers as left (or base) operand and integer types as the right (exponent) operand are always performed in the uint256 (for non-negative literals) or int256 (for a negative literals) type, regardless of the type of the right (exponent) operand.
    • Division on integer literals used to truncate in Solidity prior to version 0.4.0, but it now converts into a rational number, i.e. 5 / 2 is not equal to 2, but to 2.5.
  • Solidity has a number literal type for each rational number.

    • Integer literals and rational number literals belong to number literal types.
    • All number literal expressions (i.e. the expressions that contain only number literals and operators) belong to number literal types.
      • The number literal expressions 1 + 2 and 2 + 1 both belong to the same number literal type for the rational number 3.

User defined value types:

  • A user defined value type allows creating a zero cost abstraction over an elementary value type.
    • It’s similar to an alias, but with stricter type requirements.
  • A user defined value type is defined using type C is V, where C is the name of the newly introduced type and V has to be a built-in value type (the “underlying type”).
    • C.wrap is used to convert from the underlying type to the custom type.
    • C.unwrap is used to convert from the custom type to the underlying type.
    • Explicit and implicit conversions to and from other types are disallowed.
    • The type C does not have any operators or bound member functions. Even the operator == is not defined.
  • The data-representation of values of such types are inherited from the underlying type and the underlying type is also used in the ABI.

Boolean (bool):

  • Boolean value, true or false, with logical operators !, &&, ||, ==, and !=.

Enum:

  • User-defined type for enumerating discrete values: enum NAME {LABEL1, LABEL 2, ...}.
  • They are explicitly convertible to and from all integer types but implicit conversion is not allowed.
    • The explicit conversion from integer checks at runtime that the value lies inside the range of the enum and causes a Panic error otherwise.
  • Enums require at least one member, and its default value when declared is the first member.
    • The data representation is the same as for enums in C: The options are represented by subsequent unsigned integer values starting from 0.
  • Enums cannot have more than 256 members.
  • Enums can also be declared on the file level, outside of contract or library definitions.

Fixed Point Numbers (fixed, ufixed):

  • Useless as of v0.8.17.
  • Fixed-point numbers, declared with (u)fixedMxN where M represents the number of bits taken by the type and N represents how many decimal points are available.
    • M must be divisible by 8 and goes from 8 to 256 bits.
    • N must be between 0 and 80, inclusive.
    • ufixed and fixed are aliases for ufixed128x18 and fixed128x18, respectively.
  • Operators:
    • Comparisons: <=, <, ==, !=, >=, > (evaluate to bool)
    • Arithmetic operators: +, -, unary -, *, /, % (modulo)
  • The main difference between floating point (IEEE 754 numbers) and fixed point numbers is that the number of bits used for the integer and the fractional part (the part after the decimal dot) is flexible in the former, while it is strictly defined in the latter.
    • Generally, in floating point almost the entire space is used to represent the number, while only a small number of bits define where the decimal point is.
  • Fixed point numbers are not fully supported by Solidity yet. They can be declared, but cannot be assigned to or from.
    • 只能声明,除此以外不能做赋值等任何操作(v0.8.17: UnimplementedFeatureError: Not yet implemented - FixedPointType)。

Reference Types

Struct:

  • User-defined data containers for grouping variables: struct NAME {TYPE1 VARIABLE1; TYPE2 VARIABLE2; ...}
  • It is not possible for a struct to contain a member of its own type as the size of the struct has to be finite.
    • The struct itself can be the value type of a mapping member or it can contain a dynamically-sized array of its type.

Mapping:

  • Hash lookup tables for key => value pairs: mapping(KeyType => ValueType) VariableName
    • KeyType can be any built-in value type, bytes, string, or any contract or enum type. Other user-defined or complex types, such as mappings, structs or array types are not allowed.
    • ValueType can be any type, including mappings, arrays and structs.
  • Think of mappings as hash tables, which are virtually initialized such that every possible key exists from the beginning and is mapped to a value whose byte-representation is all zeros, a type’s default value.
    • Mappings do not keep track of the keys that were assigned a non-zero value.
    • 无法作为返回值,键值对的集合捞不出来。
    • The key data is not stored in a mapping, only its keccak256 hash is used to look up the value.
    • Because of this, mappings do not have a length or a concept of a key or value being set, and therefore cannot be erased without extra information regarding the assigned keys.
      • If a mapping is used as the base type of a dynamic storage array, deleting or popping the array will have no effect over the mapping elements.
      • The same happens if a mapping is used as the type of a member field of a struct that is the base type of a dynamic storage array.
      • The mapping is also ignored in assignments of structs or arrays containing a mapping.
    • If your mapping information must be deleted, consider using a library similar to iterable mapping , allowing you to traverse the keys and delete their values in the appropriate mapping.
  • Mappings can only have a data location of storage and thus are allowed for state variables, as storage reference types in functions, or as parameters for library functions. They cannot be used as parameters or return parameters of contract functions that are publicly visible.
    • These restrictions are also true for arrays and structs that contain mappings.
  • Mark state variables of mapping type as public and Solidity creates a getter.
    • The KeyType becomes a parameter for the getter.
    • If ValueType is a value type or a struct, the getter returns ValueType.
    • If ValueType is an array or a mapping, the getter has one parameter for each KeyType, recursively.
      • 多重索引。
  • Cannot iterate over mappings.

Arrays:

  • An array of any type, either fixed or dynamic.

    • T[k] and T[] are always arrays with elements of type T, even if T is itself an array.
      • uint32[][5] is a fixed-size array of five dynamic arrays of unsigned integers.
  • Array members:

    • length contains the number of elements.
      • The length of memory arrays is fixed (but dynamic, i.e. it can depend on runtime parameters) once they are created.
    • push() appends a zero-initialized element at the end of the array
      • Dynamic storage arrays and bytes (not string) have this method.
      • It returns a reference to the element, so that it can be used like x.push().t = 2 or x.push() = b.
      • Increasing the length of a storage array by calling push() has constant gas costs because storage is zero-initialized.
    • push(x) appends a given element at the end of the array.
      • Dynamic storage arrays and bytes (not string) have this method.
      • The function returns nothing.
    • pop() removes an element from the end of the array.
      • Dynamic storage arrays and bytes (not string) have this method.
      • This also implicitly calls delete on the removed element.
      • The function returns nothing.
      • Decreasing the length by calling pop() has a cost that depends on the “size” of the element being removed.
        • If that element is an array, it can be very costly, because it includes explicitly clearing the removed elements similar to calling delete on them.
  • Memory arrays with dynamic length can be created using the new operator.

    uint[] memory a = new uint[](7);
    
    bool[2][] pairsOfFlags;
    delete pairsOfFlags;                // clear the array completely
    pairsOfFlags = new bool[2][](0);    // identical effect
    
    • As opposed to storage arrays, it is not possible to resize memory arrays (e.g. the .push member functions are not available). You either have to calculate the required size in advance or create a new memory array and copy every element.
  • An array literal is a comma-separated list of one or more expressions, enclosed in square brackets.

    • It is always a statically-sized memory array whose length is the number of expressions.

    • The base type of the array is the type of the first expression on the list such that all other expressions can be implicitly converted to it.

      • It is a type error if this is not possible.
    • It is not enough that there is a type all the elements can be converted to. One of the elements has to be of that type.

    • Since fixed-size memory arrays of different type cannot be converted into each other (even if the base types can), you always have to specify a common base type explicitly if you want to use two-dimensional array literals:

      // uint24[2][4] is an array containing 4 elements of type uint24[2]
      function f() public pure returns (uint24[2][4] memory) {
          uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
      
          // The following does not work, because some of the inner arrays are not of the right type.
          // 0x1, 0xff, 0xffff 都不是 uint24 类型
          uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
          return x;
      }
      
    • Fixed size memory arrays cannot be assigned to dynamically-sized memory arrays.

      • If you want to initialize dynamically-sized arrays, you have to assign the individual elements.
  • Dangling reference to storage array elements:

    • A dangling reference is a reference that points to something that no longer exists or has been moved without updating the reference.

      uint[][] s;
      function f() public {
          uint[] storage ptr = s[s.length - 1];   // points to the last array element of s
          s.pop();                                // removes the last array element of s
          ptr.push(0x42);                         // writes to the array element that is no longer within the array
          // Adding a new element to s now will not add an empty array, 
          // but will result in an array of length 1 with 0x42 as element.
          s.push();
          assert(s[s.length - 1][0] == 0x42);
      }
      
      • The compiler assumes that unused storage is always zeroed. The s.push() will not explicitly write zeroes to storage, so the last element of s after that push() will have length 1 and contain 0x42 as its first element.
    • Solidity does not allow to declare references to value types in storage. These kinds of explicit dangling references are restricted to nested reference types.

    • Dangling references can occur temporarily when using complex expressions in tuple assignments:

      uint[] s;
      uint[] t;
      constructor() {
          // Push some initial values to the storage arrays.
          s.push(0x07);
          t.push(0x03);
      }
      function g() internal returns (uint[] storage) {
          s.pop();
          return t;
      }
      function f() public returns (uint[] memory) {
          // The following will first evaluate s.push() to a reference to a new element
          // at index 1. Afterwards, the call to g pops this new element, resulting in
          // the left-most tuple element to become a dangling reference. The assignment still
          // takes place and will write outside the data area of s.
          (s.push(), g()[0]) = (0x42, 0x17);
          // A subsequent push to s will reveal the value written by the previous
          // statement, i.e. the last element of s at the end of this function will have
          // the value 0x42.
          s.push();
          return s;
      }        
      
      • It is always safer to only assign to storage once per statement and to avoid complex expressions on the left-hand-side of an assignment.
    • .push() on a bytes array may switch from short to long layout in storage.

      bytes x = "012345678901234567890123456789"; // 30 bytes; length is 30
      function test() external returns(uint) {
          (x.push(), x.push()) = (0x01, 0x02);
          return x.length;
      }
      
      • When the first x.push() is evaluated, x is still stored in short layout, thereby x.push() returns a reference to an element in the first storage slot of x.
      • The second x.push() switches the bytes array to large layout (32 or more bytes long).
        • The element the second x.push() referred to is in the data area of the array, but the reference still points to its original location, which is now a part of the length field and the assignment will effectively garble the length of x.
      • To be safe, only enlarge bytes arrays by at most one element during a single assignment and do not simultaneously index-access the array in the same statement.
    • Any code with dangling references should be considered to have undefined behaviour. This means that any future version of the compiler may change the behaviour of code that involves dangling references.

  • Array slices are a view on a contiguous portion of an array.

    • They are written as x[start:end], where start and end are expressions resulting in a uint256 type (or implicitly convertible to it).
      • Both start and end are optional: start defaults to 0 and end defaults to the length of the array.
      • If start is greater than end or if end is greater than the length of the array, an exception is thrown.
      • The first element of the slice is x[start] and the last element is x[end - 1].
    • Array slices do not have any members.
    • Array slices are implicitly convertible to arrays of their underlying type and support index access.
      • Index access is not absolute in the underlying array, but relative to the start of the slice.
    • Array slices do not have a type name, which means no variable can have an array slices as type, they only exist in intermediate expressions.
    • As of v0.8.17, array slices are only implemented for calldata arrays.
  • As all variables in Solidity, the elements of newly allocated arrays are always initialized with the default value.

Dynamically-sized byte array:

  • Variable-sized arrays of bytes, declared with bytes or string.
    • Variables of type bytes and string are special arrays.
  • The bytes type is similar to bytes1[], but it is packed tightly in calldata and memory.
    • Using bytes1[] in memory adds 31 padding bytes between the elements; In storage, the padding is absent due to tight packing.
    • bytes.concat function can concatenate an arbitrary number of bytes or bytes1 ... bytes32 values.
      • The function returns a single bytes memory array that contains the contents of the arguments without padding.
      • If you want to use string parameters or other types that are not implicitly convertible to bytes, you need to convert them to bytes or bytes1 ... bytes32 first.
      • Calling bytes.concat without arguments they return an empty array.
  • string is equal to bytes but does not allow length or index access.
    • Access the the low-level bytes of the UTF-8 representation (not the individual characters) of a string s:

      bytes(s).length
      bytes(s)[7]
      
    • Compare two strings by their keccak256-hash using keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)).

    • Concatenate two strings using string.concat(s1, s2).

      • The function returns a single string memory array that contains the contents of the arguments without padding.
      • If you want to use parameters of other types that are not implicitly convertible to string, you need to convert them to string first.
      • Calling string.concat without arguments they return an empty array.
  • As a general rule, use bytes for arbitrary-length raw byte data and string for arbitrary-length string (UTF-8) data.
    • If you can limit the length to a certain number of bytes, always use one of the value types bytes1 to bytes32 because they are much cheaper.

String literals:

  • String literals are written with either double or single-quotes ("foo" or 'bar').
  • String literals can also be split into multiple consecutive parts ("foo" "bar" is equivalent to "foobar") which can be helpful when dealing with long strings.
  • String literals do not imply trailing zeroes as in C; "foo" represents three bytes, not four.
  • As with integer literals, their type can vary, but they are implicitly convertible to bytes1, …, bytes32, if they fit, to bytes and to string.
    • With bytes32 samevar = "stringliteral" the string literal is interpreted in its raw byte form when assigned to a bytes32 type.
  • String literals can only contain printable ASCII characters.
  • Unicode literals – prefixed with the keyword unicode – can contain any valid UTF-8 sequence.
    • string memory a = unicode"Hello 😃";
  • Hexadecimal literals are prefixed with the keyword hex and are enclosed in double or single-quotes (hex"001122FF", hex'0011_22_FF').
    • The content must be hexadecimal digits which can optionally use a single underscore as separator between byte boundaries.
    • The value of the literal will be the binary representation of the hexadecimal sequence.
    • Multiple hexadecimal literals separated by whitespace are concatenated into a single literal.
      • hex"00112233" hex"44556677" is equivalent to hex"0011223344556677".
    • Hexadecimal literals behave like string literals and have the same convertibility restrictions.

Address Types

Two types of address:

  • address: Holds a 20 byte value (size of an Ethereum address).
    • Explicit conversions to and from address are allowed for uint160, integer literals, bytes20 and contract types.
    • Mixed-case hexadecimal numbers conforming to EIP-55 (pass the address checksum test) are automatically treated as literals of the address type.
      • Hexadecimal literals that are between 39 and 41 digits long and do not pass the checksum test produce an error.
  • address payable: Same as address, but with the additional members transfer and send.

The idea behind this distinction is that address payable is an address you can send Ether to, while you are not supposed to send Ether to a plain address, for example because it might be a smart contract that was not built to accept Ether.

  • Implicit conversions from address payable to address are allowed.
  • Only expressions of type address and contract-type can be converted to the type address payable via the explicit conversion payable(...).
    • For contract-type, this conversion is only allowed if the contract can receive Ether, i.e., the contract either has a receive or a payable fallback function.
    • If you convert a type that uses a larger byte size to an address, for example bytes32, then the address is truncated.
      • To reduce conversion ambiguity, starting with version 0.4.24, the compiler will force you to make the truncation explicit in the conversion.
      • For the 32-byte value 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC, address(uint160(bytes20(b))) results in 0x111122223333444455556666777788889999aAaa; address(uint160(uint256(b))) results in 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc.
    • payable(0) is valid and is an exception to this rule.
  • If you need a variable of type address and plan to send Ether to it, then declare its type as address payable to make this requirement visible. Also, try to make this distinction or conversion as early as possible.

Any address, either passed as an input or cast from a contract object, has a number of attributes and methods:

  • address.balance (uint256): The balance of the address, in wei.

    • The current contract balance is address(this).balance.
  • <address payable>.transfer(uint256 amount): Send given amount of Wei to <address payable>, reverts on failure, forwards 2300 gas stipend, not adjustable.

    • 2300 is the amount of gas a contract’s fallback function receives if it’s called via transfer or send.
    • If x is a contract address, its code (more specifically: its receive function, if present, or otherwise its fallback function, if present) will be executed together with the transfer call.
      • If that execution runs out of gas or fails in any way, the Ether transfer will be reverted and the current contract will stop with an exception.
      • This is a feature of the EVM and cannot be prevented.
  • <address payable>.send(uint256 amount) returns (bool): Send given amount of Wei to <address payable>, returns false on failure, forwards 2300 gas stipend, not adjustable.

    • If the execution fails, the current contract will not stop with an exception.
    • There are some dangers in using send: The transfer fails if the call stack depth is at 1024 (this can always be forced by the caller) and it also fails if the recipient runs out of gas.
    • So in order to make safe Ether transfers, always check the return value of send, use transfer or even better: Use a pattern where the recipient withdraws the money.
  • address.call(bytes memory) returns (bool, bytes memory): Low-level CALL function—can construct an arbitrary message call with a data payload. Returns false on error. Forwards all available gas, adjustable.

    bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
    (bool success, bytes memory returnData) = address(nameReg).call(payload);
    address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
    require(success);
    
    • Avoid using .call() whenever possible when executing another contract function as it bypasses type checking, function existence check, and argument packing.
  • address.delegatecall(bytes memory) returns (bool, bytes memory): Low-level DELEGATECALL function, returns success condition and return data, forwards all available gas, adjustable.

    • If state variables are accessed via a low-level delegatecall, the storage layout of the two contracts must align in order for the called contract to correctly access the storage variables of the calling contract by name.
      • This is of course not the case if storage pointers are passed as function arguments as in the case for the high-level libraries.
  • address.staticcall(bytes memory) returns (bool, bytes memory): Low-level STATICCALL, returns success condition and return data, forwards all available gas, adjustable.

    • It’s basically the same as call, but will revert if the called function modifies the state in any way.

Considerations:

  • Don’t use <code>transfer()</code> or <code>send()</code>
  • The EVM considers a call to a non-existing contract to always succeed. So Solidity includes an extra check using the extcodesize opcode when performing external calls. This ensures that the contract that is about to be called either actually exists (it contains code) or an exception is raised.
    • This check is skipped if the return data will be decoded after the call and thus the ABI decoder will catch the case of a non-existing contract.
    • The low-level calls which operate on addresses rather than contract instances (i.e. .call(), .delegatecall(), .staticcall(), .send() and .transfer()) do not include this check, which makes them cheaper in terms of gas but also less safe.
  • All three functions call, delegatecall and staticcall are very low-level functions and should only be used as a last resort as they break the type-safety of Solidity.
    • The gas option is available on all three methods, while the value option is only available on call.
      • It is best to avoid relying on hardcoded gas values in your smart contract code, regardless of whether state is read from or written to, as this can have many pitfalls. Also, access to gas might change in the future.

Operators: <=, <, ==, !=, >= and >.

Contract Types

Every contract defines its own type:

  • You can implicitly convert contracts to contracts they inherit from.
  • Contracts can be explicitly converted to and from the address type.
    • Explicit conversion to and from the address payable type is only possible if the contract type has a receive or payable fallback function. The conversion is still performed using address(x).
    • If the contract type does not have a receive or payable fallback function, the conversion to address payable can be done using payable(address(x)).
  • If you declare a local variable of contract type (MyContract c), you can call functions on that contract.
  • The members of contract types are the external functions of the contract including any state variables marked as public.
  • For a contract C you can use type(C) to access type information about the contract.
  • Contracts do not support any operators.

Units and Globally Available Variables

Ether units:

  • A literal number can take a suffix of wei, gwei or ether to specify a subdenomination of Ether.

    • Ether numbers without a postfix are assumed to be Wei.
    assert(1 wei == 1);
    assert(1 gwei == 1e9);
    assert(1 ether == 1e18);
    

Time units:

  • Suffixes like seconds, minutes, hours, days, weeks after literal numbers can be used to specify units of time; seconds are the base unit.

     assert(1 == 1 seconds);
    
    • Not every year equals 365 days and not even every day has 24 hours because of leap seconds. Due to the fact that leap seconds cannot be predicted, an exact calendar library has to be updated by an external oracle.

When a contract is executed in the EVM, it has access to a small set of global objects. These include the block, msg, and tx objects. In addition, Solidity exposes a number of EVM opcodes as predefined functions.

The msg object is the transaction call (EOA originated) or message call (contract originated) that launched this contract execution.

  • msg.sender (address): It represents the address that initiated this contract call.
    • If the contract was called directly by an EOA transaction, then this is the address that signed the transaction, but otherwise it will be a contract address.
    • Whenever a contract calls another contract, the values of all the attributes of msg change to reflect the new caller’s information.
      • The only exception to this is the delegatecall function, which runs the code of another contract/library within the original msg context.
  • msg.value (uint): The value of ether sent with this call (in wei).
  • msg.data (bytes calldata): Complete calldata.
  • msg.sig (bytes4): First four bytes of the calldata (i.e. function identifier)
  • The values of all members of msg (including msg.sender and msg.value) can change for every external function call. This includes calls to library functions.

The tx object provides a means of accessing transaction-related information:

  • tx.origin (address): Sender of the transaction (full call chain)
    • The address of the originating EOA for this transaction. WARNING: unsafe!

The block object contains information about the current block:

  • blockhash(uint blockNumber) returns (bytes32): hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero.
    • The block hashes are not available for all blocks for scalability reasons. You can only access the hashes of the most recent 256 blocks, all other values will be zero.
  • block.timestamp (uint): current block timestamp as seconds since Unix epoch.

When contracts are evaluated off-chain rather than in context of a transaction included in a block, you should not assume that block.* and tx.* refer to values from any specific block or transaction. These values are provided by the EVM implementation that executes the contract and can be arbitrary.

Mathematical and cryptographic functions:

  • addmod(uint x, uint y, uint k) returns (uint), mulmod(uint x, uint y, uint k) returns (uint): For modulo addition and multiplication.

    • addmod(x,y,k) calculates (x + y) % k.
    • Does not wrap around at 2**256.
    • Assert that k != 0 starting from version 0.5.0.
    // wrap around
    // https://go.dev/play/p/uv2bLEM3QMm
    var u uint8 = 255
    u++
    fmt.Printf("%[1]d\n%08[1]b\n", u)
    //  0
    //  00000000
    
    var i int8 = 127
    i++
    fmt.Printf("%[1]d\n%08[1]b\n", i)
    // -128
    // -10000000
    
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address): Recover the address associated with the public key from elliptic curve signature or return zero on error.

    • r = first 32 bytes of signature
    • s = second 32 bytes of signature
    • v = final 1 byte of signature
    • ecrecover returns an address, and not an address payable.
    • A valid signature can be turned into a different valid signature without requiring knowledge of the corresponding private key.
      • This is usually not a problem unless you require signatures to be unique or use them to identify items.
      • OpenZeppelin have a ECDSA helper library that you can use as a wrapper for ecrecover without this issue.

Contract related:

  • this (current contract’s type): The current contract, explicitly convertible to address.
  • selfdestruct(address payable recipient): Destroy the current contract, sending its funds to the given address and end execution.
    • The receiving contract’s receive function is not executed.
    • The contract is only really destroyed at the end of the transaction and revert might “undo” the destruction.

Operators

delete

delete a assigns the initial value for the type to a.

  • For integers it is equivalent to a = 0.

  • For arrays it assigns a dynamic array of length zero or a static array of the same length with all elements set to their initial value.

    • It will only reset a itself, not the value it referred to previously.
    • delete a[x] deletes the item at index x of the array and leaves all other elements and the length of the array untouched.
      • It leaves a gap in the array.
        • If you plan to remove items, a mapping is probably a better choice.
  • For structs, it assigns a struct with all members reset.

    • The value of a after delete a is the same as if a would be declared without assignment, with the following caveat:
      • delete has no effect on mappings, as the keys of mappings may be arbitrary and are generally unknown. So if you delete a struct, it will reset all members that are not mappings and also recurse into the members unless they are mappings.
        • Individual keys and what they map to can be deleted: If a is a mapping, then delete a[x] will delete the value stored at x.
  • delete a really behaves like an assignment to a, i.e. it stores a new object in a.

    pragma solidity >=0.4.0 <0.9.0;
    contract DeleteExample {
        uint data;
        uint[] dataArray;   // state variable
        function f() public {
            uint x = data;
            delete x;       // sets x to 0, does not affect data
            delete data;    // sets data to 0, does not affect x
            uint[] storage y = dataArray; // y is local variable (assign a reference)
            // this sets dataArray.length to zero, but as uint[] is a complex object, also
            // y is affected which is an alias to the storage object
            delete dataArray; 
            assert(y.length == 0);
            // On the other hand: "delete y" is not valid, as assignments to local variables
            // referencing storage objects can only be made from existing storage objects.
        }
    }
    

Functions

Function Types

Function types come in two flavours - internal and external functions:

  • function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
  • Internal functions can only be called inside the current contract (more specifically, inside the current code unit, which also includes internal library functions and inherited functions) because they cannot be executed outside of the context of the current contract.
    • Calling an internal function is realized by jumping to its entry label, just like when calling a function of the current contract internally.
    • By default, function types are internal, so the internal keyword can be omitted.
      • This only applies to function types. Visibility has to be specified explicitly for functions defined in contracts, they do not have a default.
  • External functions consist of an address and a function signature and they can be passed via and returned from external function calls.
    • .address returns the address of the contract of the function.
    • .selector returns the ABI function selector

Conversions: A function type A is implicitly convertible to a function type B if and only if their parameter types are identical, their return types are identical, their internal/external property is identical and the state mutability of A is more restrictive than the state mutability of B:

  • pure functions can be converted to view and non-payable functions.
  • view functions can be converted to non-payable functions.
  • payable functions can be converted to non-payable functions.
    • If a function is payable, this means that it also accepts a payment of zero Ether, so it also is non-payable.
    • A non-payable function will reject Ether sent to it, so non-payable functions cannot be converted to payable functions.
  • No other conversions between function types are possible.

If a function type variable is not initialized, calling it results in a Panic error.

  • The same happens if you call a function after using delete on it.

If external function types are used outside of the context of Solidity, they are treated as the function type, which encodes the address followed by the function identifier together in a single bytes24 type.

External functions of the current contract can be used both as an internal and as an external function.

  • To use f as an internal function, just use f.
  • If you want to use its external form, use this.f.

Functions of the current contract can be called directly (“internally”), also recursively.

  • Internal function calls are translated into simple jumps inside the EVM.
    • This has the effect that the current memory is not cleared, i.e. passing memory references to internally-called functions is very efficient.
  • Only functions of the same contract instance can be called internally.
  • Avoid excessive recursion, as every internal function call uses up at least one stack slot and there are only 1024 slots available.

Functions can also be called using the this.g(8); and c.g(2); notation; c is a contract instance and g is a function belonging to c.

  • Calling the function g via either way results in it being called “externally”, using a message call and not directly via jumps.

    • Function calls on this cannot be used in the constructor, as the actual contract has not been created yet.
    • A function call from one contract to another does not create its own transaction, it is a message call as part of the overall transaction.
  • Functions of other contracts have to be called externally.

  • For an external call, all function arguments have to be copied to memory.

  • When calling functions of other contracts, you can specify the amount of Wei or gas sent with the call.

    contract InfoFeed {
        function info() public payable returns (uint ret) { return 42; }
    }
    contract Consumer {
        InfoFeed feed;
        function setFeed(InfoFeed addr) public { feed = addr; }
        function callFeed() public { feed.info{value: 10, gas: 800}(); }
    }    
    
    • feed.info{value: 10, gas: 800} only locally sets the value and amount of gas sent with the function call, and the parentheses at the end perform the actual call.
      • feed.info{value: 10, gas: 800} does not call the function and the value and gas settings are lost.
    • You need to use the modifier payable with the info function because otherwise, the value option would not be available.

Functions can be defined inside and outside of contracts.

Functions outside of a contract, also called “free functions”, always have implicit internal visibility.

  • Their code is included in all contracts that call them, similar to internal library functions.
  • Functions defined outside a contract are still always executed in the context of a contract.
    • They still can call other contracts, send them Ether and destroy the contract that called them, among other things.
  • The main difference to functions defined inside a contract is that free functions do not have direct access to the variable this, storage variables and functions not in their scope.

Functions within A Contract

function FunctionName([parameters]) {public|private|internal|external} [pure|constant|view|payable] [modifiers] [returns (return types)]

The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).

  • A contract can have at most one receive function, declared using receive() external payable { ... } (without the function keyword).
  • This function cannot have arguments, cannot return anything and must have external visibility and payable state mutability.
  • It can be virtual, can override and can have modifiers.
  • If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
  • If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception.
  • In the worst case, the receive function can only rely on 2300 gas being available (for example when send or transfer is used), leaving little room to perform other operations except basic logging.
    • The following operations will consume more gas than the 2300 gas stipend:
      • Writing to storage
      • Creating a contract
      • Calling an external function which consumes a large amount of gas
      • Sending Ether

The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all (plain Ether transfer) and there is no receive Ether function.

  • A contract can have at most one fallback function, declared using either fallback() external [payable] or fallback(bytes calldata input) external [payable] returns (bytes memory output) (both without the function keyword).
  • The fallback function always receives data, but in order to also receive Ether it must be marked payable.
  • This function must have external visibility.
  • A fallback function can be virtual, can override and can have modifiers.
  • In the worst case, if a payable fallback function is used in place of a receive function, it can only rely on 2300 gas being available.

Solidity knows two kinds of function calls: external ones that do create an actual EVM message call and internal ones that do not. Furthermore, internal functions can be made inaccessible to derived contracts. This gives rise to four types of visibility for functions:

  • public: The default visibility; public functions can be called by other contracts or EOA transactions, or from within the contract.
  • external: External functions can be called by other contracts or EOA transactions; they cannot be called from within the contract unless explicitly prefixed with the keyword this.
  • internal: Internal functions are only accessible from within the contract and can be called by derived contracts (those that inherit this one).
    • Since they are not exposed to the outside through the contract’s ABI, they can take parameters of internal types. This includes the types listed below and any composite types that recursively contain them:
      • mappings,
      • internal function types,
      • reference types with location set to storage,
      • multi-dimensional arrays (applies only to ABI coder v1),
      • structs (applies only to ABI coder v1).
    • This restriction for non-internal functions does not apply to library functions because of their different internal ABI.
  • private: Private functions are like internal functions but cannot be called by derived contracts.

State-mutability-related keywords:

  • pure: A pure function is one that neither reads nor writes any variables in storage. It can only operate on arguments and return data, without reference to any stored data.
    • Pure functions are intended to encourage declarative-style programming without side effects or state.
  • view: A function marked as a view promises not to modify any state.
    • The following statements are considered modifying the state:
      • Writing to state variables.
      • Emitting events.
      • Creating other contracts.
      • Using selfdestruct.
      • Sending Ether via calls.
      • Calling any function not marked view or pure.
      • Using low-level calls.
      • Using inline assembly that contains certain opcodes.

view 函数在其它合约函数的内部被调用,这种场景下对 view 的调用会消耗 gas。

  • view 被 EOA 调用时,只会读取本地节点的数据;在其它合约函数的内部被调用时,对应的交易需要被挖矿,view 函数在所有全节点都要被执行(执行 view 函数对应的 opcode 也成了所有要被计价的 opcodes 中的一部分),消耗了整个网络的计算资源,所以会消耗 gas。

A payable function is one that can accept incoming payments.

  • Functions not declared as payable will reject incoming payments.
  • There are two exceptions, due to design decisions in the EVM: coinbase payments and SELFDESTRUCT inheritance will be paid even if the fallback function is not declared as payable, but this makes sense because code execution is not part of those payments anyway.

A function modifier is a special type of function, which is most often used to create conditions that apply to many functions within a contract in a declarative way.

  • The placeholder statement, denoted by a single underscore character _, is used to denote where the body of the function being modified should be inserted.

    • The _ symbol can appear in the modifier multiple times. Each occurrence is replaced with the function body.

      contract Demo {
          uint256 public N;
          modifier doubled() {
              _;
              _;
          }
          function setN() public doubled {
              N += 1;
          }
      }
      
  • Explicit returns from a modifier or function body only leave the current modifier or function body.

    • Return variables are assigned and control flow continues after the _ in the preceding modifier (as there might be multiple _ in the preceding modifier).
    • An explicit return from a modifier with return; does not affect the values returned by the function.
    • The modifier can, however, choose not to execute the function body at all and in that case the return variables are set to their default values just as if the function had an empty body.
  • Multiple modifiers are applied to a function by specifying them in a whitespace-separated list and are evaluated in the order presented.

  • Inside a modifier, you can access all the values (variables and arguments) visible to the modified function.

    • The values can only be passed to them explicitly at the point of invocation. Modifiers cannot implicitly access or change the arguments and return values of functions they modify.
    • Arbitrary expressions are allowed for modifier arguments and in this context, all symbols visible from the function are visible in the modifier.
    • Symbols introduced in the modifier are not visible in the function (as they might change by overriding).
  • If you want to access a modifier m defined in a contract C, you can use C.m to reference it without virtual lookup.

    • It is only possible to use modifiers defined in the current contract or its base contracts.
  • Modifiers can also be defined in libraries but their use is limited to functions of the same library.

  • Modifiers are inheritable properties of contracts and may be overridden by derived contracts, but only if they are marked virtual.

A contract can have multiple functions of the same name but with different parameter types. This process is called “overloading” and also applies to inherited functions.

  • Overloaded functions are selected by matching the function declarations in the current scope to the arguments supplied in the function call.

    • Return parameters are not taken into account for overload resolution.
    • Functions are selected as overload candidates if all arguments can be implicitly converted to the expected types.
    • If there is not exactly one candidate, resolution fails.
    function f(uint8 val) public pure returns (uint8 out) {
        out = val;
    }
    function f(uint256 val) public pure returns (uint256 out) {
        out = val;
    }
    
    • Calling f(50) would create a type error since 50 can be implicitly converted both to uint8 and uint256 types.
    • f(256) would resolve to f(uint256) overload as 256 cannot be implicitly converted to uint8.
  • It is an error if two externally visible functions differ by their Solidity types but not by their external types.

    // This will not compile
    contract A {
        function f(B value) public pure returns (B out) {
            out = value;
        }
        function f(address value) public pure returns (address out) {
            out = value;
        }
    }
    contract B {
    }
    
    • Both f function overloads above end up accepting the address type for the ABI although they are considered different inside Solidity.

Contracts

Contracts in Solidity are similar to classes in object-oriented languages. They contain persistent data in state variables, and functions that can modify these variables.

  • Calling a function on a different contract (instance) will perform an EVM function call and thus switch the context such that state variables in the calling contract are inaccessible.
  • A contract and its functions need to be called for anything to happen. There is no “cron” concept in Ethereum to call a function at a particular event automatically.

A contract’s code cannot be changed, but a contract can be “deleted,” removing the code and its internal state (storage) from its address, leaving a blank account. Contracts are destroyed by a special EVM opcode called SELFDESTRUCT. In Solidity, this opcode is exposed as a high-level built-in function called selfdestruct that takes one argument: the address to receive any ether balance remaining in the contract account.

  • selfdestruct(address recipient);
  • This operation costs “negative gas,” a gas refund, thereby incentivizing the release of network client resources from the deletion of stored state.
  • You must explicitly add this command to your contract if you want it to be deletable—this is the only way a contract can be deleted, and it is not present by default.
  • Any transactions sent to that account address after the contract has been deleted do not result in any code execution, because there is no longer any code there to execute.
  • If someone sends Ether to removed contracts, the Ether is forever lost.
    • If you want to deactivate your contracts, you should instead disable them by changing some internal state which causes all functions to revert. This makes it impossible to use the contract, as it returns Ether immediately.

Creating Contracts

A contract can create other contracts using the new keyword.

contract D {
    uint public x;
    constructor(uint a) payable {
        x = a;
    }
}
contract C {
    D d = new D(4);
    function createD(uint arg) public {
        D newD = new D(arg);
        newD.x();
    }
    function createAndEndowD(uint arg, uint amount) public payable {
        D newD = new D{value: amount}(arg); // Send ether along with the creation
        newD.x();
    }
}
  • The full code of the contract being created has to be known when the creating contract is compiled so recursive creation-dependencies are not possible.
  • It is possible to send Ether while creating other contracts using the value option, but it is not possible to limit the amount of gas.
  • If the creation fails (due to out-of-stack, not enough balance or other problems), an exception is thrown.

When creating a contract, the address of the contract is computed from the address of the creating contract and a counter (nonce) that is increased with each contract creation. If you specify the option salt (a bytes32 value), then contract creation will use a different mechanism to come up with the address of the new contract:

contract D {
    uint public x;
    constructor(uint a) {
        x = a;
    }
}
contract C {
    function createDSalted(bytes32 salt, uint arg) public {
        // This complicated expression just tells you how the address
        // can be pre-computed. It is just there for illustration.
        // You actually only need ``new D{salt: salt}(arg)``.
        address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(abi.encodePacked(
                type(D).creationCode,
                abi.encode(arg)
            ))
        )))));

        D d = new D{salt: salt}(arg);
        require(address(d) == predictedAddress);
    }
}
  • It will compute the address from the address of the creating contract, the given salt value, the (creation) bytecode of the created contract and the constructor arguments.
    • The counter (“nonce”) is not used.
  • This enables the derivation the address of the new contract before it is created. We can rely on this address also in case the creating contracts creates other contracts in the meantime.
    • The main use-case here is contracts that act as judges for off-chain interactions, which only need to be created if there is a dispute.
    • A contract can be re-created at the same address after having been destroyed.
  • It is possible for that newly created contract to have a different deployed bytecode even though the creation bytecode has been the same (which is a requirement because otherwise the address would change). This is due to the fact that the constructor can query external state that might have changed between the two creations and incorporate that into the deployed bytecode before it is stored.

Inheritance

Solidity’s contract object supports multiple inheritance including polymorphism for extending a base contract with additional functionality.

Polymorphism means that a function call (internal and external) always executes the function of the same name (and parameter types) in the most derived contract in the inheritance hierarchy.

  • It is possible to call functions further up in the inheritance hierarchy internally by explicitly specifying the contract using ContractName.functionName() or using super.functionName() if you want to call the function one level higher up in the flattened inheritance hierarchy.

When a contract inherits from other contracts, only a single contract is created on the blockchain, and the code from all the base contracts is compiled into the created contract.

  • This means that all internal calls to functions of base contracts also just use internal function calls (super.f(..) will use JUMP and not a message call).

State variable shadowing is considered as an error. A derived contract can only declare a state variable x, if there is no visible state variable with the same name in any of its bases.

Function overriding:

  • Base functions can be overridden by inheriting contracts to change their behavior if they are marked as virtual.
    • Functions with the private visibility cannot be virtual.
    • Functions without implementation have to be marked virtual outside of interfaces.
      • In interfaces, all functions are automatically considered virtual.
  • The overriding function must then use the override keyword in the function header.
    • Starting from Solidity 0.8.8, the override keyword is not required when overriding an interface function, except for the case where the function is defined in multiple bases.
  • The overriding function may only change the visibility of the overridden function from external to public.
  • The mutability may be changed to a more strict one following the order:
    • non-payable can be overridden by view and pure.
    • view can be overridden by pure.
    • payable is an exception and cannot be changed to any other mutability.
  • For multiple inheritance, the most derived base contracts that define the same function must be specified explicitly.
  • If a contract inherits the same function from multiple (unrelated) bases, it has to explicitly override it.
  • If you do not mark a function that overrides as virtual, derived contracts can no longer change the behaviour of that function.
  • Public state variables can override external functions if the parameter and return types of the function matches the getter function of the variable.

Modifier overriding:

  • Function modifiers can override each other.
  • The virtual keyword must be used on the overridden modifier and the override keyword must be used in the overriding modifier.

The constructor function is used to initialize the state of the contract.

  • The constructor is run in the same transaction as the contract creation.
    • A contract’s life cycle starts with a creation transaction from an EOA or contract account.
  • After the constructor has run, the final code of the contract is deployed to the blockchain.
    • This code includes all functions that are part of the public interface and all functions that are reachable from there through function calls.
    • It does not include the constructor code or internal functions that are only called from the constructor.
  • The constructor function is optional.
    • If there is no constructor, the contract will assume the default constructor, which is equivalent to constructor() {}.
    • Only one constructor is allowed, which means overloading is not supported.
  • You can use internal parameters in a constructor (for example storage pointers).
    • In this case, the contract has to be marked abstract, because these parameters cannot be assigned valid values from outside but only through the constructors of derived contracts.
    • Internally, constructor arguments are passed ABI encoded after the code of the contract itself.
  • The constructors will always be executed in the linearized order (“most base-like” to “most-derived”).
    • The order in which the base classes are given in the is directive is important: You have to list the direct base contracts in the order from “most base-like” to “most derived”.

Abstract Contracts

Contracts must be marked as abstract when at least one of their functions is not implemented or when they do not provide arguments for all of their base contract constructors. Even if this is not the case, a contract may still be marked abstract, such as when you do not intend for the contract to be created directly.

  • Even if an abstract contract itself does implement all defined functions, it can not be instantiated directly.
  • Abstract contracts decouple the definition of a contract from its implementation providing better extensibility and self-documentation and facilitating patterns like the template method and removing code duplication.
  • Abstract contracts are useful in the same way that defining methods in an interface is useful. It is a way for the designer of the abstract contract to say “any child of mine must implement this method”.
// function without implementation
function foo(address) external returns (address);
// a declaration of a variable whose type is a function type
function(address) external returns (address) foo;

Interfaces

An interface definition is structured exactly like a contract, except none of the functions are defined, they are only declared.

  • This type of declaration is often called a stub; it tells you the functions’ arguments and return types without any implementation.
  • An interface specifies the “shape” of a contract; when inherited, each of the functions declared by the interface must be defined by the child.
  • All functions declared in interfaces are implicitly virtual and any functions that override them do not need the override keyword.
    • An overriding function can be overridden only if the overriding function is marked virtual.
  • Interfaces can inherit from other interfaces. This has the same rules as normal inheritance.
  • Contracts can inherit interfaces as they would inherit other contracts.

Interfaces are similar to abstract contracts, but they cannot have any functions implemented. There are further restrictions:

  • They cannot inherit from other contracts, but they can inherit from other interfaces.
  • All declared functions must be external in the interface, even if they are public in the contract.
  • They cannot declare a constructor.
  • They cannot declare state variables.
  • They cannot declare modifiers.

Libraries

A library contract is one that is meant to be deployed only once at a specific address and used by other contracts, using the delegatecall method.

  • If library functions are called, their code is executed in the context of the calling contract, i.e. this points to the calling contract, and especially the storage from the calling contract can be accessed.
  • Library functions can only be called directly (i.e. without the use of DELEGATECALL) if they do not modify the state (i.e. if they are view or pure functions), because libraries are assumed to be stateless.
    • If a library’s code is executed using a CALL instead of a DELEGATECALL, it will revert unless a view or pure function is called.
    • The EVM does not provide a direct way for a contract to detect whether it was called using CALL or not, but a contract can use the ADDRESS opcode to find out “where” it is currently running. The generated code compares this address to the address used at construction time to determine the mode of calling.
      • The runtime code of a library always starts with a push instruction, which is a zero of 20 bytes at compilation time.
      • When the deploy code runs, this constant is replaced in memory by the current address and this modified code is stored in the contract.
      • At runtime, this causes the deploy time address to be the first constant to be pushed onto the stack and the dispatcher code compares the current address against this constant for any non-view and non-pure function.
      • This means that the actual code stored on chain for a library is different from the code reported by the compiler as deployedBytecode.
  • It is possible to obtain the address of a library by converting the library type to the address type, i.e. using address(LibraryName).

In comparison to contracts, libraries are restricted in the following ways:

  • They cannot have state variables.
  • They cannot inherit nor be inherited.
  • They cannot receive Ether.
  • They cannot be destroyed.

using for

The directive using A for B; can be used to attach functions (A) as member functions to any type (B). These functions will receive the object they are called on as their first parameter.

  • A can be one of:
    • A list of file-level or library functions (using {f, g, h, L.t} for uint;) - only those functions will be attached to the type.
      • The type (uint) has to be implicitly convertible to the first parameter of each of these functions. This check is performed even if none of these functions are called.
    • The name of a library (using L for uint;) - all functions (both public and internal ones) of the library are attached to the type.
      • Even functions where the type of the first parameter does not match the type of the object are attached. The type is checked at the point the function is called and function overload resolution is performed.
  • At file level, B has to be an explicit type (without data location specifier). Inside contracts, you can also use using L for *;, which has the effect that all functions of the library L are attached to all types.

All external library calls are actual EVM function calls. This means that if you pass memory or value types, a copy will be performed, even in case of the self variable. The only situation where no copy will be performed is when storage reference variables are used or when internal library functions are called.

Calling Other Contracts

The safest way to call another contract is if create that other contract yourself using the keyword new.

  • new will create the contract on the blockchain and return an object that you can use to reference it.

Another way you can call a contract is by casting the address of an existing instance of the contract.

  • With this method, you apply a known interface to an existing instance.
  • It is critically important that you know, for sure, that the instance you are addressing is in fact of the type you assume.

Solidity offers some more “low-level” functions for calling other contracts. These correspond directly to EVM opcodes of the same name and allow us to construct a contract-to-contract call manually.

  1. call
contract Token is mortal {
    constructor(address _faucet) {
        if !(_faucet.call("withdraw", 0.1 ether)) {
            revert("Withdrawal from faucet failed");
        }
    }
}
  • call will return false if there is a problem.
  • This type of call is a blind call into a function, very much like constructing a raw transaction, only from within a contract’s context.
  • It can expose your contract to a number of security risks, most importantly reentrancy.
  1. delegatecall
    • delegatecall runs the code of another contract inside the context of the execution of the current contract.
    • When you call a library, the call is always delegatecall and runs within the context of the caller.
    • It is most often used to invoke code from a library.
    • It also allows you to draw on the pattern of using library functions stored elsewhere, but have that code work with the storage data of your contract.
    • It can have some unexpected effects, especially if the contract you call was not designed as a library.

Error Handling

When a contract terminates with an error, all the state changes (changes to variables, balances, etc.) are reverted, all the way up the chain of contract calls if more than one contract was called. This ensures that transactions are atomic.

The transfer function will fail with an error and revert the transaction if there is insufficient balance to make the transfer.

When exceptions happen in a sub-call, they “bubble up” (i.e., exceptions are rethrown) automatically unless they are caught in a try/catch statement.

  • Exceptions to this rule are send and the low-level functions call, delegatecall and staticcall: they return false as their first return value in case of an exception instead of “bubbling up”.

Additional error-checking code will increase gas consumption slightly. Find the right balance between gas consumption and verbose error checking based on the expected use of your contract.

Exceptions can contain error data that is passed back to the caller in the form of error instances. The built-in errors Error(string) and Panic(uint256) are used. Error is used for “regular” error conditions; Panic is used for errors that should not be present in bug-free code.

  • The assert function creates an error of type Panic(uint256).
  • The require function either creates an error without any data or an error of type Error(string).
    • The require function is evaluated just as any other function. This means that all arguments are evaluated before the function itself is executed.
      • In particular, in require(condition, f()) the function f is executed even if condition is true.
    • It is currently not possible to use custom errors in combination with require. Use if (!condition) revert CustomError(); instead.
  • In both cases, the caller can react on such failures using try/catch, but the changes in the callee will always be reverted.
  • Internally, Solidity performs a revert operation (instruction 0xfd). This causes the EVM to revert all changes made to the state.

A direct revert can be triggered using the revert statement and the revert function.

  • The revert statement takes a custom error as direct argument without parentheses.
    • Using a custom error instance will usually be much cheaper than a string description, because you can use the name of the error to describe it, which is encoded in only four bytes.
    • A longer description can be supplied via NatSpec which does not incur any costs.
  • For backwards-compatibility reasons, there is also the revert() function, which uses parentheses and accepts a string.

A failure in an external call can be caught using a try/catch statement.

  • The try keyword has to be followed by an expression representing an external function call or a contract creation (new ContractName()).
    • Errors inside the expression are not caught (for example if it is a complex expression that also involves internal function calls), only a revert happening inside the external call itself.
    • The optional returns part that follows declares return variables matching the types returned by the external call.
      • In case there was no error, these variables are assigned and the contract’s execution continues inside the first success block. If the end of the success block is reached, execution continues after the catch blocks.
  • An error can only be caught when coming from an external call, reverts happening in internal calls or inside the same function cannot be caught.

Errors cannot be overloaded or overridden but are inherited.

Checked or Unchecked Arithmetic

Prior to Solidity 0.8.0, arithmetic operations would always wrap in case of under- or overflow, leading to widespread use of libraries that introduce additional checks. Since Solidity 0.8.0, all arithmetic operations revert on over- and underflow by default, thus making the use of these libraries unnecessary.

To obtain the previous behaviour, an unchecked block can be used.

  • The unchecked block can be used everywhere inside a block, but not as a replacement for a block.
  • It also cannot be nested.
  • The setting only affects the statements that are syntactically inside the block. Functions called from within an unchecked block do not inherit the property.
  • To avoid ambiguity, you cannot use _; inside an unchecked block.
  • It is not possible to disable the check for division by zero or modulo by zero using the unchecked block.
  • Bitwise operators do not perform overflow or underflow checks.
  • Explicit type conversions will always truncate and never cause a failing assertion with the exception of a conversion from an integer to an enum type.

Events

When a transaction completes (successfully or not), it produces a transaction receipt. It contains log entries that provide information about the actions that occurred during the execution of the transaction.

Log entries provide the contract’s address, a series of up to four topics and some arbitrary length binary data.

  • Events leverage the existing function ABI in order to interpret this (together with an interface spec) as a properly typed structure.

Given an event name and series of event parameters, we split them into two sub-series:

  1. The indexed
    • Those which are indexed, which may number up to 3 (for non-anonymous events) or 4 (for anonymous ones), are used alongside the Keccak hash of the event signature to form the topics of the log entry.
  2. The un-indexed
    • Those which are not indexed form the byte array of the event.

Solidity events give an abstraction on top of the EVM’s logging functionality.

  • Events are the Solidity high-level objects that are used to construct logs.
  • Events are inheritable members of contracts.
  • Applications can subscribe and listen to these events through the RPC interface of an Ethereum client.

Event objects take arguments that are serialized and recorded in the transaction logs, in the blockchain.

  • These logs are associated with the address of the contract, are incorporated into the blockchain, and stay there as long as a block is accessible (forever as of now, but this might change with Serenity).
    • You can filter events by the address of the contract that emitted the event.
  • The Log and its event data is not accessible from within contracts (not even from the contract that created them).
  • It is possible to request a Merkle proof for logs, so if an external entity supplies a contract with such a proof, it can check that the log actually exists inside the blockchain.

You can add the keyword indexed to up to 3 three parameters, to make the value part of an indexed table (hash table) that can be searched or filtered by an application.

  • This adds them to a special data structure known as “topics” instead of the data part of the log.
    • All parameters without the indexed attribute are ABI-encoded into the data part of the log.
  • A topic can only hold a single word (32 bytes) so if you use a reference type for an indexed argument, the Keccak-256 hash of the value is stored as a topic instead.

Misc

p.recipient.call.value(p.amount)(p.data)

Every external function call in Solidity can be modified in two ways:

  1. You can add Ether together with the call.
    • Within p.recipient.call.value(p.amount)(p.data), the low-level function call is used to invoke another contract with p.data as payload and p.amount Wei is sent with that call.
      • p.data 为空,即没有 calldata,那就是单纯的 ether 转账行为。
  2. You can limit the amount of gas available to the call.
    • f.gas(2).value(20)() calls the modified function f and thereby sending 20 Wei and limiting the gas to 2.

Type Information

The expression type(X) can be used to retrieve information about the type X.

  • type(C).name: The name of the contract.
  • type(C).creationCode: Memory byte array that contains the creation bytecode of the contract.
    • This can be used in inline assembly to build custom creation routines, especially by using the create2 opcode.
    • This property can not be accessed in the contract itself or any derived contract. It causes the bytecode to be included in the bytecode of the call site and thus circular references like that are not possible.
  • type(C).runtimeCode: Memory byte array that contains the runtime bytecode of the contract.
    • This is the code that is usually deployed by the constructor of C.
      • If C has a constructor that uses inline assembly, this might be different from the actually deployed bytecode.
    • Also note that libraries modify their runtime bytecode at time of deployment to guard against regular calls.
    • The same restrictions as with .creationCode also apply for this property.

Currently, there is limited support for this feature (X can be either a contract or an integer type) but it might be expanded in the future.


References