With the Solidity 0.8.x series being just around the corner, we would like to provide insights into the upcoming breaking changes that will come with it.
We want to provide a preview release binary for everyone to try out so that you can give your feedback.
The main change for 0.8.x is the switch to checked arithmetic operations by default. This means that x + y will throw an exception on overflow. In other words: You will not need SafeMath anymore!
Since the scope of 0.8.x is not yet fully specified, you still have a chance of influencing what will be included and how! To do so you can either create a new issue or voice your opinion on an existing issue
The current tentatively accepted changes are those in the "Implementation Backlog" column of our project board that are labeled as "Breaking Change".
You can already browse the updated documentation, which should be consistent with the behaviour of the compiler. If you find any errors, please open an issue!
0.8.x Preview Binaries!
You can find the preview binaries at the following locations:
Note that all of these compilers will display a warning about them being a prerelease version, which is intentional.
If you use solc-js, just copy the binary as soljson.js into the root directory of the solc-js npm module.
You can also try out the 0.8.x compiler in Remix. Just select the "Solidity compiler" tab on the left to switch to the compiler sidebar and click the inconspicuous + icon above the drop-down with the list of available compilers. Paste the soljson.js URL into the "URL" field in the dialog that pops up and confirm with "OK". You should now see custom as the selected version and be able to use it to compile your code. In case of problems please refer to Remix documentation on the compiler module.
Make sure to use pragma solidity >0.7.0 - otherwise Remix might switch back to a different version.
For other ways to compile Solidity contracts, copy the binaries to the appropriate places.
Checked Arithmetic
The "Checked Arithmetic" feature of Solidity 0.8.x consists of three sub-features:
- Revert on assertion failures and similar conditions instead of using the invalid opcode.
- Actual checked arithmetic.
- unchecked blocks.
Revert on assertion failures and similar conditions instead of using the invalid opcode
Previously, internal errors like division by zero, failing assertions, array access out of bounds, etc., would result in an invalid opcode being executed. The idea was to distinguish these more critical errors from non-critical errors that lie outside of the domain of the Smart Contract programmer like invalid input, not enough balance for a token transfer, and so on. These non-critical errors use the revert opcode and optionally add an error description.
The problem with the invalid opcode is that, in contrast to the revert opcode, it consumes all gas available. This makes it very expensive and you should try to avoid it at all cost.
We wanted to consider arithmetic overflow as a critical error, but did not want to cause it to consume all gas. As a middle ground, we chose to use the revert opcode, but provide a different error data so that static (and dynamic) analysis tools can easily distinguish them.
More specifically:
- Non-critical errors will revert either with empty error data or with error data that corresponds to the ABI-encoding of a function call to a function with the signature Error(string).
- Critical errors revert with error data that corresponds to the ABI-encoding of a function call to a function with the signature Panic(uint256).
Because of that, we also use the term "panic" for such critical errors.
To distinguish the two situations, you can take a look at the first four bytes of the return data in case of a failure. If it is 0x4e487b71, then it is a panic, if it is 0x08c379a0 or if the message is empty, it is a "regular" error.
Panics use specific error codes to distinguish certain situations in which the panic is triggered. This is the current list of error codes:
- 0x01: If you call assert with an argument that evaluates to false.
- 0x11: If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.
- 0x12: If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).
- 0x21: If you convert a value that is too big or negative into an enum type.
- 0x31: If you call .pop() on an empty array.
- 0x32: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).
- 0x41: If you allocate too much memory or create an array that is too large.
- 0x51: If you call a zero-initialized variable of internal function type.
This list of codes can be extended in the future.
Actual Checked Arithmetic
By default, all arithmetic operations will perform overflow and underflow checks (Solidity already had division by zero checks). In case of an underflow or overflow, a Panic(0x11) error will be thrown and the call will revert.
The following code, for example, will trigger such an error:
contract C { function f() public pure { uint x = 0; x--; } }
The checks are based on the actual type of the variable, so even if the result would fit an EVM word of 256 bits, if your type is uint8 and if the result is larger than 255, it will trigger a Panic.
We did not introduce checks for explicit type conversions from larger to smaller types, because we think that such checks might be unexpected. We would love to get your input on that matter!
The following operations now have checks that they did not have before:
- addition (+), subtraction (-), multiplication (*)
- increment and decrement (++ / --)
- unary negation (-)
- exponentiation (**)
- division (see below) (/)
There are some edge cases you might not expect. For example, unary negation is only available for signed types. The problem is that in two's complement representation, there is one more negative number for each bit width than there are positive numbers. This means that the following code will trigger a Panic:
function f() pure { int8 x = -128; -x; }
The reason is that 128 does not fit an 8-bit signed integer.
For the same reason, signed division can result in an assertion:
function f() pure { int8 x = -128; x/(-1); }
Everyone who wanted to write a SafeMath function for exponentiation probably noticed that it is rather expensive to do that because the EVM does not provide overflow signalling. Essentially you have to implement your own exp routine without using the exp opcode for the general case.
We hope that we found a rather efficient implementation and would also appreciate your feedback about that!
For many special cases, we actually implemented it using the exp opcode instead of our own implementation. More specifically, exponentiation operations that use a literal number as base will use the exp opcode directly. There are also specialized code paths for bases that are variables with small value: All bases up to 2, bases between 3 and 10 and bases between 11 and 306 get special treatment and should result in a cheap exp opcode.
unchecked Blocks
Since checked arithmetic uses more gas, as discussed in the previous section, and because there are times where you really want the wrapping behaviour, for example when implementing cryptographic routines, we provide a way to disable the checked arithmetic and re-enable the previous "wrapping" or "modulo" arithmetic:
Any block of the form unchecked { ... } uses wrapping arithmetic. You can use this special block as a regular statement inside another block:
contract C { function f() public pure returns (uint) { uint x = 0; unchecked { x--; } return x; } }
An unchecked block can of course contain any number or any kind of other statements and will create its own variable scope. If there is a function call inside the unchecked block, the function will not inherit the setting - you have to use another unchecked block inside the function if you also want it to use wrapping arithmetic.
We decided for some restrictions for the unchecked block:
-
In modifiers you cannot use _; inside it, because this might lead to confusion about whether the property is inherited.
-
You can only use it inside a block but not as a replacement for a block. The following code snippets, for example, are both invalid:
function f() unchecked { uint x = 7; }
function f() pure { uint x; for (uint i = 0; i < 100; i ++) unchecked { x += i; }
You have to wrap the unchecked blocks inside another block to make it work.
If you find this inconvenient, but also if you find the current behaviour good, please let us know!
Full Changelog (Subject to More Additions)
Breaking Changes:
- Assembler: The artificial ASSIGNIMMUTABLE opcode and the corresponding builtin in the "EVM with object access" dialect of Yul take the base offset of the code to modify as additional argument.
- Code Generator: All arithmetic is checked by default. These checks can be disabled using unchecked { ... }.
- Code Generator: Use revert with error signature Panic(uint256) and error codes instead of invalid opcode on failing assertions.
- General: Remove global functions log0, log1, log2, log3 and log4.
- Parser: Exponentiation is right associative. a**b**c is parsed as a**(b**c).
- Type System: Disallow explicit conversions from negative literals and literals larger than type(uint160).max to address type.
- Type System: Unary negation can only be used on signed integers, not on unsigned integers.
Language Features:
- Super constructors can now be called using the member notation e.g. M.C(123).
AST Changes:
- New Node IdentifierPath replacing in many places the UserDefinedTypeName
- New Node: unchecked block - used for unchecked { ... }.
We hope you find this preview releases helpful and look forward to hearing your thoughts on the implementation of the breaking changes. If you are interested in discussing language design with us, make sure to join the solidity-users mailing list!