Secureum Book
  • 🛡️Secureum Bootcamp
    • 🛡️Secureum Bootcamp
    • 🙌Participate
    • 📜History
  • 📚LEARN
    • Introduction
      • 🔷1. Ethereum Basics
        • 1.1 Ethereum: Concept, Infrastructure & Purpose
        • 1.2 Properties of the Ethereum Infrastructure
        • 1.3 Ethereum vs. Bitcoin
        • 1.4 Ethereum Core Components
        • 1.5 Gas Metering: Solving the Halting Problem
        • 1.6 web2 vs. web3: The Paradigm Shift
        • 1.7 Decentralization
        • 1.8 Cryptography, Digital Signature & Keys
        • 1.9 Ethereum State & Account Types
        • 1.10 Transactions: Properties & Components
        • 1.11 Contract Creation
        • 1.12 Transactions, Messages & Blockchain
        • 1.13 EVM (Ethereum Virtual Machine) in Depth
        • 1.14 Transaction Reverts & Data
        • 1.15 Block Explorer
        • 1.16 Mainnet & Testnets
        • 1.17 ERCs & EIPs
        • 1.18 Legal Aspects in web3: Pseudonymity & DAOs
        • 1.19 Security in web3
        • 1.20 web2 Timescales vs. web3 Timescales
        • 1.21 Test-in-Prod. SSLDC vs. Audits
        • Summary: 101 Keypoints
      • 🌀2. Solidity
        • 2.1 Solidity: Influence, Features & Layout
        • 2.2 SPDX & Pragmas
        • 2.3 Imports
        • 2.4 Comments & NatSpec
        • 2.5 Smart Contracts
        • 2.6 State Variables: Definition, Visibility & Mutability
        • 2.7 Data Location
        • 2.8 Functions
        • 2.9 Events
        • 2.10 Solidity Typing
        • 2.11 Solidity Variables
        • 2.12 Address Type
        • 2.13 Conversions
        • 2.14 Keywords & Shorthand Operators
        • 2.15 Solidity Units
        • 2.16 Block & Transaction Properties
        • 2.17 ABI Encoding & Decoding
        • 2.18 Error Handling
        • 2.19 Mathematical & Cryptographic Functions
        • 2.20 Control Structures
        • 2.21 Style & Conventions
        • 2.22 Inheritance
        • 2.23 EVM Storage
        • 2.24 EVM Memory
        • 2.25 Inline Assembly
        • 2.26 Solidity Version Changes
        • 2.27 Security Checks
        • 2.28 OpenZeppelin Libraries
        • 2.29 DAppSys Libraries
        • 2.30 Important Protocols
        • Summary: 201 Keypoints
      • 🔏3. Security Pitfalls & Best Practices
        • 3.1 Solidity Versions
        • 3.2 Access Control
        • 3.3 Modifiers
        • 3.4 Constructor
        • 3.5 Delegatecall
        • 3.6 Reentrancy
        • 3.7 Private Data
        • 3.8 PRNG & Time
        • 3.9 Math & Logic
        • 3.10 Transaction Order Dependence
        • 3.11 ecrecover
        • 3.12 Unexpected Returns
        • 3.13 Ether Accounting
        • 3.14 Transaction Checks
        • 3.15 Delete Mappings
        • 3.16 State Modification
        • 3.17 Shadowing & Pre-declaration
        • 3.18 Gas & Costs
        • 3.19 Events
        • 3.20 Unary Expressions
        • 3.21 Addresses
        • 3.22 Assertions
        • 3.23 Keywords
        • 3.24 Visibility
        • 3.25 Inheritance
        • 3.26 Reference Parameters
        • 3.27 Arbitrary Jumps
        • 3.28 Hash Collisions & Byte Level Issues
        • 3.29 Unicode RTLO
        • 3.30 Variables
        • 3.31 Pointers
        • 3.32 Out-of-range Enum
        • 3.33 Dead Code & Redundant Statements
        • 3.34 Compiler Bugs
        • 3.35 Proxy Pitfalls
        • 3.36 Token Pitfalls
        • 3.37 Special Token Pitfalls
        • 3.38 Guarded Launch Pitfalls
        • 3.39 System Pitfalls
        • 3.40 Access Control Pitfalls
        • 3.41 Testing, Unused & Redundand Code
        • 3.42 Handling Ether
        • 3.43 Application Logic Pitfalls
        • 3.44 Saltzer & Schroeder's Design Principles
        • Summary: 201 Keypoints
      • 🗜️4. Audit Techniques & Tools
        • 4.1 Audit
        • 4.2 Analysis Techniques
        • 4.3 Specification, Documentation & Testing
        • 4.4 False Positives & Negatives
        • 4.5 Security Tools
        • 4.6 Audit Process
        • Summary: 101 Keypoints
      • ☝️5. Audit Findings
        • 5.1 Criticals
        • 5.2 Highs
        • 5.3 Mediums
        • 5.4 Lows
        • 5.5 Informationals
        • Summary: 201 Keypoints
  • 🌱CARE
    • CARE
      • CARE Reports
  • 🚩CTFs
    • A-MAZE-X CTFs
      • Secureum A-MAZE-X
      • Secureum A-MAZE-X Stanford
      • Secureum A-MAZE-X Maison de la Chimie Paris
Powered by GitBook
On this page
  • Exceptions
  • Error Signatures: error and panic
  • panic
  • error
  • Error Handling Primitives
  • assert
  • require
  • revert
  • try/catch
  • catch Blocks
  • try/catch State Change
  • External Call Failure
  1. LEARN
  2. Introduction
  3. 2. Solidity

2.18 Error Handling

Error handling is one of the most important fundamental and critical aspects of programming languages' security. The reason is that errors during program execution are what result in security vulnerabilities. These could be errors resulting from user inputs when they interact with the smart contract and the inputs are not as expected by the developer during the coding of the contract.

In the EVM, an exception undoes or reverses all the changes made to the state of the smart contract in the context of the current transaction: the calls and all the subcalls that may be several levels deep. In addition, an error is also flagged to the caller so that they can take appropriate action.

They could also be due to assumptions made within the smart contract that are not really valid for the various control and data flows that happen during program execution. They could also be related to the programming variants that are expected from a specification perspective and these invariants might not hold good during certain control and data flows.

Exceptions

So when exceptions happen within subcalls in that call hierarchy, during runtime they bubble up, and What this means is that exceptions are rethrown at the higher level calls automatically.

There are some exceptions to this rule. There are some differences here in the context of the send primitive, and the low level function calls: call, delegatecall and staticcall which we talked about earlier. These primitives (send, call, delegatecall and staticcall) return a boolean true or false as their first return value instead of an exception bubbling up.

This is an important distinction to be kept in mind when analyzing smart contracts because the exception behavior is different for these primitives, compared to the standard message calls. Exceptions that happen in external calls made during the contact execution can be caught with the try catch statement.

These exceptions can contain data that is passed back to the caller and this data consists of a function selector indicating which function the exception happened in, and also some other ABI encoded data that gives more information about the exception.

Error Signatures: error and panic

Solidity supports two error signatures: error and panic. error takes a string parameter whereas panic takes an unsigned parameter. error is meant to be used for "regular error conditions", such as input validation and so on. panic is used for errors that should not be present in bug free code.

panic

The panic exception is generated in various situations in Solidity, and the error code supplied with the error data indicates the kind of panic.

There are many of these error codes. Some of them are

  • 0x01: indicates that assert has an argument that evaluated to false.

  • 0x11: an overflow or underflow happened in arithmetic.

  • 0x12: division by zero or modulus by zero occured.

  • 0x31: pop() of an empty array occurred.

  • 0x32: out of bounds access for an array.

There are numerous error codes for panic...

This error reverts all the state changes made to the contract logic so far in the context of the transaction that triggered it.

error

Error string exception, as discussed earlier, are generated when require (which we'll see shortly) executes and its argument evaluates to false.

The error string is also generated in other situations such as an external call made to a contact that contains no code, or if the contract receives ether via public function without the payable modifier. Or if the contract receives ether via a public getter function.

Error Handling Primitives

Solidity supports multiple primitives for error handling, being the first set of primitives are functions that let the developer assert or require certain conditions to be held.

assert

The assert(x) primitive for example, specifies a condition x as its argument, and if that condition is not met (if it evaluates to false during runtime), The assert primitive is meant to be used for internal errors for program invariants that should never be violated within the smart contract if it does not have bugs as intended by the developer.

These asserts result in the panic errors that take the uint256 type, to reiterate they should be used for internal errors for checking invariants, normal code bug free code should never cause panics.

require

It is another error handling primitive supported by Solidity. Similarly to assert it also specifies a condition that gets evaluated at runtime, and if that condition evaluates to false, then it again raises a revert, that reverses all the state changes made to the contract so far. The require primitive takes an optional string as an argument: this is a message that gets printed if the required condition is not met.

Therefore, it either creates an error of type error string or an error without any error data.

The require primitive is meant to be used for errors in inputs from users which should be validated to make sure they are within the thresholds of what is acceptable with the smart contract logic, so some sanity checks on those values are necessary (this is a fundamental pillar of security).

require is also used for checking the return values from calls that are made to external contracts. So any type of external interaction, be it inputs from users or return values from external contact calls, are what require is meant to be used with. Require takes in an optional message string that is output when the condition fails.

The thing to be kept in mind is the difference between assert and require. There were some historical differences as well in the use of a particular opcode: a different opcode for assert versus require that affects the Gas consumption, but some of these have been changed in the recent Solidity versions.

revert

Finally there is a revert primitive that unconditionally aborts execution when triggered, reverting all the state changes similar to when the conditions are not met for a certain require primitive.

There are two ways to explicitly trigger a revert in Solidity: using the revert CustomError(arg1,...) primitive or the revert([string]) function, where the string parameter is optional.

In both these cases, the execution is aborted and all the state changes made. as part of the transaction, are reversed. This distinction between CustomError and the string, is interesting from an optimization and usecase perspective.

try/catch

These primitives supported by Solidity are fundamental error handling. The syntax is

try Expr [returns()] {...}
catch <Block> {...}

So, we have the try and catch keywords coupled with an expression that contains an external function call or creation of a contract. These are coupled with code blocks corresponding to the success blocks or the catch blocks. These are code segments within the curly braces shown above. Which block gets executed depends on whether there was a failure or not in that external call within that expression.

If there were no errors then the success block gets executed (the block that immediately follows the try expression in the syntax shown earlier). But if there was an error in that external call then the catch block, or one of the catch blocks, gets executed. Which catch block gets executed depends on the error type, and there are multiple of them.

catch Blocks

As mentioned, Solidity supports different kinds of catch blocks depending on the error type. There is a catch block that supports an error string. catch Error(<string reason>). This is executed if the error was caused by revert with a reason string, or require where the condition evaluated to false.

Then there is a catch kind that supports panic error code catch Panic(uint <error code>). If this error was caused by a panic failing assert, (remember: division by zero, outer bound array accesses, arithmetic underflow/overflow) this is the catch block that will be run.

In addition there is a catch that specifies the low level data catch (bytes <LowLevelData>). This one gets executed if the error signature does not match any other clause shown above. Or if there was an error while decoding the error message itself, or if no error data was provided with that exception. This variable that is declared the low-level data gives us access to the error data in that case.

Finally if the developer is not interested in the type of error data, one can simply use catch as is. These give various options to deal with different types of exceptions, that might come from the external call that is used within the try catch permit.

try/catch State Change

As we just discussed, there exists the concept of the success block, that gets executed when there are no exceptions in that external call. There are also error blocks that correspond to the different catch blocks which get executed when there are exceptions encountered in that external call.

If execution reaches the success block, it means that there were no exceptions in that external call, and all the state changes that are done in the context of the external call are committed to the state of the contract.

But if execution reaches one of the catch or error blocks, then it means that the state changes in that external call context have been reverted, because of the exception. There could also be a context where the try catch statement itself reverts for reasons of decoding or low level failures.

External Call Failure

These failures of the external call made in the context of the catch primitive, could happen for a variety of reasons and one cannot always assume that the error message is coming directly from the contract that was called in that external call, because the error could happen deeper down in the call chain resulting from that call. It was forwarded to the point where it was received.

This could also be due to an out of Gas (OOG) situation, in which case the caller still has a bit of Gas to deal with that exception because not all of it is forwarded to the callee.

Previous2.17 ABI Encoding & DecodingNext2.19 Mathematical & Cryptographic Functions

Last updated 1 year ago

📚
🌀