Programming with Solidity
Contents
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
orblock.number
) or execution data (msg.value
orgasleft()
) 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
andmulmod
are allowed, even though, with the exception ofkeccak256
, they do call external contracts.
- Any expression that accesses storage, blockchain data (e.g.
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.
- This means you cannot initialize immutables inline with a value that depends on another immutable.
- 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.
- Immutables that are assigned at their declaration are only considered initialized once the constructor of the contract is executing.
- 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
andimmutable
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 thanimmutable
values.
- Due to this,
- For these values, 32 bytes are reserved, even if they would fit in fewer bytes.
- For a
- 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.
- When used within the same contract, the external access (e.g.
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
isfalse
. - The default value for the
uint
orint
types is0
. - For statically-sized arrays and
bytes1
tobytes32
, each individual element will be initialized to the default value corresponding to its type. - For dynamically-sized arrays,
bytes
andstring
, 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 fromint8
touint256
.- Without a size suffix, 256-bit quantities are used, to match the word size of the EVM.
- For an integer type
X
, you can usetype(X).min
andtype(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 remainderr
after the division of the operanda
by the operandn
, whereq = int(a / n)
andr = 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 as1
.- In checked mode, exponentiation only uses the comparatively cheap
exp
opcode for small bases.- For the cases of
x**3
, the expressionx*x*x
might be cheaper. - In any case, gas cost tests and the use of the optimizer are advisable.
- For the cases of
- The result of a shift operation has the type of the left operand, truncating the result to match the type.
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
and1.3
(but not1.
).
- Examples include
-
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 toM * 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
, hexadecimal0x2eff_abde
, scientific decimal notation1_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 integer4
(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 typeuint128
, the expression2.5 + a
has to have a proper type. Since there is no common type for the type of2.5
anduint128
, the Solidity compiler does not accept this code.
- Because
-
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)
or255 + [1, 2, 3][0]
are computed within the typeuint8
and can overflow.
- Expressions like
-
-
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) orint256
(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 to2
, but to2.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
and2 + 1
both belong to the same number literal type for the rational number 3.
- The number literal expressions
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
, whereC
is the name of the newly introduced type andV
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
orfalse
, 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.
- The explicit conversion from integer checks at runtime that the value lies inside the range of the
- 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
.
- The data representation is the same as for enums in C: The options are represented by subsequent unsigned integer values starting from
- 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
whereM
represents the number of bits taken by the type andN
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
andfixed
are aliases forufixed128x18
andfixed128x18
, respectively.
- Operators:
- Comparisons:
<=
,<
,==
,!=
,>=
,>
(evaluate tobool
) - Arithmetic operators:
+
,-
, unary-
,*
,/
,%
(modulo)
- Comparisons:
- 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 orenum
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 themapping
elements. - The same happens if a
mapping
is used as the type of a member field of astruct
that is the base type of a dynamic storage array. - The
mapping
is also ignored in assignments of structs or arrays containing amapping
.
- If a
- 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 returnsValueType
. - If
ValueType
is an array or a mapping, the getter has one parameter for eachKeyType
, recursively.- 多重索引。
- The
- Cannot iterate over mappings.
Arrays:
-
An array of any type, either fixed or dynamic.
T[k]
andT[]
are always arrays with elements of typeT
, even ifT
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.
- The length of
push()
appends a zero-initialized element at the end of the array- Dynamic storage arrays and
bytes
(notstring
) have this method. - It returns a reference to the element, so that it can be used like
x.push().t = 2
orx.push() = b
. - Increasing the length of a storage array by calling
push()
has constant gas costs because storage is zero-initialized.
- Dynamic storage arrays and
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.
- Dynamic storage arrays and
pop()
removes an element from the end of the array.- Dynamic storage arrays and
bytes
(notstring
) 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.
- If that element is an array, it can be very costly, because it includes explicitly clearing the removed elements similar to calling
- Dynamic storage arrays and
-
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.
- As opposed to storage arrays, it is not possible to resize memory arrays (e.g. the
-
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 ofs
after thatpush()
will have length1
and contain0x42
as its first element.
- The compiler assumes that unused storage is always zeroed. The
-
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 abytes
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, therebyx.push()
returns a reference to an element in the first storage slot ofx
. - 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 ofx
.
- The element the second
- 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.
- When the first
-
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]
, wherestart
andend
are expressions resulting in auint256
type (or implicitly convertible to it).- Both
start
andend
are optional:start
defaults to0
andend
defaults to the length of the array. - If
start
is greater thanend
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 isx[end - 1]
.
- Both
- 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.
- They are written as
-
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
orstring
.- Variables of type
bytes
andstring
are special arrays.
- Variables of type
- The
bytes
type is similar tobytes1[]
, but it is packed tightly incalldata
andmemory
.- Using
bytes1[]
inmemory
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 ofbytes
orbytes1 ... 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 tobytes
orbytes1 ... bytes32
first. - Calling
bytes.concat
without arguments they return an empty array.
- The function returns a single
- Using
string
is equal tobytes
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 tostring
first. - Calling
string.concat
without arguments they return an empty array.
- The function returns a single
-
- As a general rule, use
bytes
for arbitrary-length raw byte data andstring
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
tobytes32
because they are much cheaper.
- If you can limit the length to a certain number of bytes, always use one of the value types
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, tobytes
and tostring
.- With
bytes32 samevar = "stringliteral"
the string literal is interpreted in its raw byte form when assigned to abytes32
type.
- With
- 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 tohex"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.
- Explicit conversions to and from address are allowed for
address payable
: Same asaddress
, but with the additional memberstransfer
andsend
.
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
toaddress
are allowed. - Only expressions of type
address
and contract-type can be converted to the typeaddress payable
via the explicit conversionpayable(...)
.- 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 examplebytes32
, then theaddress
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 in0x111122223333444455556666777788889999aAaa
;address(uint160(uint256(b)))
results in0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
.
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 asaddress 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
.
- The current contract balance is
-
<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
orsend
. - If
x
is a contract address, its code (more specifically: itsreceive
function, if present, or otherwise its fallback function, if present) will be executed together with thetransfer
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.
- 2300 is the amount of gas a contract’s fallback function receives if it’s called via
-
<address payable>.send(uint256 amount) returns (bool)
: Send given amount of Wei to<address payable>
, returnsfalse
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
, usetransfer
or even better: Use a pattern where the recipient withdraws the money.
-
address.call(bytes memory) returns (bool, bytes memory)
: Low-levelCALL
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.
- Avoid using
-
address.delegatecall(bytes memory) returns (bool, bytes memory)
: Low-levelDELEGATECALL
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.
- If state variables are accessed via a low-level
-
address.staticcall(bytes memory) returns (bool, bytes memory)
: Low-levelSTATICCALL
, 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.
- It’s basically the same as
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
andstaticcall
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 thevalue
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.
- The
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 usingaddress(x)
. - If the contract type does not have a receive or payable fallback function, the conversion to
address payable
can be done usingpayable(address(x))
.
- Explicit conversion to and from the
- 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 usetype(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
orether
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 originalmsg
context.
- The only exception to this is the
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
(includingmsg.sender
andmsg.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 whenblocknumber
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 signatures
= second 32 bytes of signaturev
= final 1 byte of signatureecrecover
returns anaddress
, and not anaddress 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 toaddress
.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 indexx
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.
- It leaves a gap in the array.
- It will only reset
-
For structs, it assigns a struct with all members reset.
- The value of
a
afterdelete 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, thendelete a[x]
will delete the value stored atx
.
- Individual keys and what they map to can be deleted: If
- The value of
-
delete a
really behaves like an assignment toa
, i.e. it stores a new object ina
.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 topayable
functions.
- If a function is
- 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.
- Function calls on
-
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 theinfo
function because otherwise, thevalue
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 thefunction
keyword). - This function cannot have arguments, cannot return anything and must have
external
visibility andpayable
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 whensend
ortransfer
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 following operations will consume more gas than the 2300 gas stipend:
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]
orfallback(bytes calldata input) external [payable] returns (bytes memory output)
(both without thefunction
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 keywordthis
.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.
- 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:
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 aview
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
orpure
. - Using low-level calls.
- Using inline assembly that contains certain opcodes.
- The following statements are considered modifying the state:
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 thefallback
function is not declared aspayable
, 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.
- Return variables are assigned and control flow continues after the
-
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 contractC
, you can useC.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 since50
can be implicitly converted both touint8
anduint256
types. f(256)
would resolve tof(uint256)
overload as256
cannot be implicitly converted touint8
.
-
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 theaddress
type for the ABI although they are considered different inside Solidity.
- Both
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 usingsuper.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 useJUMP
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 bevirtual
. - Functions without implementation have to be marked
virtual
outside of interfaces.- In interfaces, all functions are automatically considered
virtual
.
- In interfaces, all functions are automatically considered
- Functions with the
- 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.
- Starting from Solidity 0.8.8, the
- The overriding function may only change the visibility of the overridden function from
external
topublic
. - The mutability may be changed to a more strict one following the order:
- non-
payable
can be overridden byview
andpure
. view
can be overridden bypure
.payable
is an exception and cannot be changed to any other mutability.
- non-
- 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 theoverride
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.
- If there is no constructor, the contract will assume the default constructor, which is equivalent to
- 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.
- In this case, the contract has to be marked
- 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”.
- The order in which the base classes are given in the
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 theoverride
keyword.- An overriding function can be overridden only if the overriding function is marked
virtual
.
- An overriding function can be overridden only if the overriding function is marked
- 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 arepublic
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 areview
orpure
functions), because libraries are assumed to be stateless.- If a library’s code is executed using a
CALL
instead of aDELEGATECALL
, it will revert unless aview
orpure
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 theADDRESS
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
.
- If a library’s code is executed using a
- It is possible to obtain the address of a library by converting the library type to the
address
type, i.e. usingaddress(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 type (
- 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.
- A list of file-level or library functions (
- At file level,
B
has to be an explicit type (without data location specifier). Inside contracts, you can also use usingL for *;
, which has the effect that all functions of the libraryL
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.
call
contract Token is mortal {
constructor(address _faucet) {
if !(_faucet.call("withdraw", 0.1 ether)) {
revert("Withdrawal from faucet failed");
}
}
}
call
will returnfalse
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.
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 functionscall
,delegatecall
andstaticcall
: they returnfalse
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 typePanic(uint256)
. - The
require
function either creates an error without any data or an error of typeError(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 functionf
is executed even ifcondition
istrue
.
- In particular, in
- It is currently not possible to use custom errors in combination with require. Use
if (!condition) revert CustomError();
instead.
- The
- 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.
- 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
- 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:
- 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.
- 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:
- You can add Ether together with the call.
- Within
p.recipient.call.value(p.amount)(p.data)
, the low-level functioncall
is used to invoke another contract withp.data
as payload andp.amount
Wei is sent with that call.- 若
p.data
为空,即没有 calldata,那就是单纯的 ether 转账行为。
- 若
- Within
- You can limit the amount of gas available to the call.
f.gas(2).value(20)()
calls the modified functionf
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.
- This can be used in inline assembly to build custom creation routines, especially by using the
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.
- If
- 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.
- This is the code that is usually deployed by the constructor of
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
- Mastering Ethereum by Andreas M. Antonopoulos and Dr. Gavin Wood (O’Reilly). Copyright 2019 The Ethereum Book LLC and Gavin Wood, 978-1-491-97194-9
- https://docs.soliditylang.org/en/v0.8.17/index.html
- https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
- https://hackmd.io/@vbuterin/evm_feature_removing