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
Prevously, 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
or23 % 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]
wherei >= x.length
ori < 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 signaturePanic(uint256)
and error codes instead of invalid opcode on failing assertions. - General: Remove global functions
log0
,log1
,log2
,log3
andlog4
. - Parser: Exponentiation is right associative.
a**b**c
is parsed asa**(b**c)
. - Type System: Disallow explicit conversions from negative literals and literals larger than
type(uint160).max
toaddress
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 theUserDefinedTypeName
- 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!