The use of revert()
, assert(), and require() in Solidity, and the new REVERT opcode in the EVM
The release of Solidity version 0.4.10 introduced the assert()
, require()
and revert()
functions, and confusion has reigned ever since.
In particular, the assert()
and require()
“guard” functions improve the readability of contract code, but differentiating between them can be quite confounding.
In this article, I’ll:
- explain the problem these functions solve.
- discuss how the Solidity compiler handles the new
assert()
,require()
andrevert()
. - Give some rules of thumb for deciding how and when to use each one.
For convenience, I’ve created a simple contract using each of these features which you can test out in remix.
If you really just want a TLDR, this answer on the ethereum stackexchange should do it.
Patterns for error handling in Solidity
The old way: throw
and the if … throw pattern
Say your contract has a few special functions, that should only be callable by a particular address which is designated as the owner
.
Prior to Solidity 0.4.10 (and for a while afterwards), this was a common pattern for enforcing permissions:
contract HasAnOwner {
address owner;
function useSuperPowers(){
if (msg.sender != owner) { throw; }
// do something only the owner should be allowed to do
}
}
If the useSuperPowers()
function is called by anyone other than owner
, the function will throw returning an invalid opcode
error, undoing all state changes, and using up all remaining gas (see this article for more on gas and fees in ethereum).
The throw keyword is now being deprecated, and eventually will be removed altogether. Fortunately, the new functions assert(), require(), and revert() provide the same functionality, with a much cleaner syntax.
Life after throw
Let’s look at how to update that if .. throw
pattern with our new guard functions.
This line:
if(msg.sender != owner) { throw; }
currently behaves exactly the same as all of the following:
if(msg.sender != owner) { revert(); }
assert(msg.sender == owner);
require(msg.sender == owner);
Note that in the assert()
and require()
examples, the conditional statement is an inversion of the if
block’s condition, switching the comparison operator !=
to ==
.
Differentiating between assert() and require()
First, to help separate these ‘guard’ functions in your mind, imagine assert()
as an overly assertive bully, who steals all your gas. Then imagine require()
as a polite managerial type, who calls out your errors, but is more forgiving.
With that mnemonic handy, what’s the real difference between these two functions?
Prior to the Byzantium network upgrade, require()
and assert()
actually behave identically, but their bytecode output is slightly different.
assert()
uses the0xfe
opcode to cause an error conditionrequire()
uses the0xfd
opcode to cause an error condition
If you look up either of those opcodes in the yellow paper, you won’t find them. This is why you see the invalid opcode
error, because there’s no specification for how a client should handle them.
That will change however after Byzantium, and the implemention of EIP-140: REVERT instruction in the Ethereum Virtual Machine . Then the0xfd
opcode will be mapped to theREVERT
instruction.
This is what I find really fascinating:
Many contracts have been deployed since version 0.4.10, which include a new opcode lying dormant, until it’s no longer invalid. At the appointed time, it will wake up, and become REVERT
!
Note: throw
andrevert()
also use 0xfd
. Prior to 0.4.10. throw
used 0xfe
.
What the REVERT opcode will do
REVERT
will still undo all state changes, but it will be handled differently than an “invalid opcode” in two ways:
- It will allow you to return a value.
- It will refund any remaining gas to the caller.
1. It will allow you to return a value
Most smart contract developers are quite familiar with the notoriously unhelpful invalid opcode
error. Fortunately, we’ll soon be able to return an error message, or a number corresponding to an error type.
That will look something like this:
revert(‘Something bad happened’);
or
require(condition, ‘Something bad happened’);
Note: solidity doesn’t support this return value argument yet, but you can watch this issue for that update.
2. Refund the remaining gas to the caller
Currently, when your contract throws it uses up any remaining gas. This can result in a very generous donation to miners, and often ends up costing users a lot of money.
Once REVERT
is implemented in the EVM, it will be plain old bad manners not to use it to refund the excess gas.
Choosing between revert(), assert() and require()
So, ifrevert()
and require()
both refund any left over gas, AND allow you to return a value, why would want to burn up gas using assert()
?
The difference lies in the bytecode output, and for this I’ll quote from the docs (emphasis mine):
The
require
function should be used to ensure valid conditions, such as inputs, or contract state variables are met, or to validate return values from calls to external contracts. If used properly, analysis tools can evaluate your contract to identify the conditions and function calls which will reach a failingassert
. Properly functioning code should never reach a failing assert statement; if this happens there is a bug in your contract which you should fix.
To clarify that somewhat: it should be considered a normal and healthy occurrence for a require()
statement to fail (same with revert()
). When an assert()
statement fails, something very wrong and unexpected has happened, and you need to fix your code.
By following this guidance, static analysis and formal verification tools will be able to examine your contracts to find and prove the conditions which could break your contract, or to prove that your contract operates as designed without flaws.
In practice, I use a few heuristics to help me decide which is appropriate.
Use require()
to:
- Validate user inputs ie.
require(input<20);
- Validate the response from an external contract ie.
require(external.send(amount));
- Validate state conditions prior to execution, ie.
require(block.number > SOME_BLOCK_NUMBER)
orrequire(balance[msg.sender]>=amount)
- Generally, you should use
require
most often - Generally, it will be used towards the beginning of a function
There are many examples of require()
in use for such things in our Smart Contract Best Practices.
Use revert()
to:
- Handle the same type of situations as
require()
, but with more complex logic.
If you have some complex nested if/else
logic flow, you may find that it makes sense to use revert()
instead of require()
. Keep in mind though, complex logic is a code smell.
Use assert()
to:
- Check for overflow/underflow, ie.
c = a+b; assert(c > b)
- Check invariants, ie.
assert(this.balance >= totalSupply);
- Validate state after making changes
- Prevent conditions which should never, ever be possible
- Generally, you will probably use
assert
less often - Generally, it will be used towards the end of a function.
Basically, require()
should be your go to function for checking conditions, assert()
is just there to prevent anything really bad from happening, but it shouldn’t be possible for the condition to evaluate to false
.
Also: “you should not use assert
blindly for overflow checking but only if you think that previous checks (either using if
or require
) would make an overflow impossible”. — comment from @chriseth
Conclusion
These functions are very powerful tools for your security toolbox. Knowing how and when to use them will not only help prevent vulnerabilities, but also make your code more user friendly, and future proof against upcoming changes.
Do you love this stuff?
I’m part of the team at ConsenSys Diligence. If you have a knack for diving deeply into Solidity and the EVM, and an interest in smart contract security, we’re looking for people to join our smart contract audit practice (apply here).
If you made it this far in the post, but don’t meet the exact criteria in the job description, that’s OK. Just include a message mentioning this post, and calling out your experience with, and interest in Ethereum.
Like this piece? Sign up here for our weekly newsletter.
Disclaimer: The views expressed by the author above do not necessarily represent the views of Consensys AG. ConsenSys is a decentralized community with ConsenSys Media being a platform for members to freely express their diverse ideas and perspectives. To learn more about ConsenSys and Ethereum, please visit our website.