CS4998: Blockchain Development Textbook
  • CS4998: Blockchain Development
  • Prerequisites
  • Introduction
    • Blockchain Theory
      • Bitcoin and the UTXO Model
      • Ethereum and the State-Based Model
    • Remix - A First Glance
    • Hello World!
      • Solidity File Structure
      • Primitive Values & Types
      • Contract Structure
      • Functions
      • Data Structures
      • Summary & Exercises
    • Hello World! Pt. 2
      • Control Flow
      • Interfaces and Inheritance
      • Constructors
      • Contract Interactions
      • Modifiers
      • Dynamic Arrays and Strings
        • Dynamic Arrays
        • Strings
      • Errors
      • Events
      • Units and Global Variables
      • Default Functions
  • Local Development
    • Node Providers
    • Interacting With On-Chain Contracts
    • Migrating to Foundry & VS Code
      • The Basics of Forge
      • Installing and Using Dependencies
      • Cast
      • Anvil
  • Understanding the EVM
    • The Ethereum Virtual Machine
      • A First Look at Computers
      • The Turing Machine
      • EVM Data Structures
      • Operation Codes (Opcodes)
      • Gas
      • Contract Compilation
      • Contract Runtime
    • Gas Optimizations
  • Yul & Advanced EVM Topics
    • Yul
    • Metamorphism
    • Bitwise Manipulations
  • Correctness
    • Security
    • Types of Testing
  • ERC Standards
    • Why ERCs?
    • ERC20
    • ERC721
    • ERC777
    • ERC1155
  • Frequently Used Smart Contracts
    • OpenZeppelin
    • Uniswap
    • Multisignature Contracts
    • AAVE/Compound
  • MEV & Advanced Blockchain Theory
    • Consensus Mechanisms vs Sybil Resistance Mechanisms
    • Maximal Extractable Value (MEV)
    • Looking Past The EVM
  • Etcetera
    • Developer Practices
    • Spring 2023 Past Resources
Powered by GitBook
On this page
  • Raising Errors via require
  • revert and assert
  1. Introduction
  2. Hello World! Pt. 2

Errors

Maintaining Invariants

In an ideal world, we want our programs to contain the logic necessary to handle all cases. X occurred? No problem, there is some code to handle X. Y occurred? Not to worry, there is also some code to handle Y. However, we do not live in an ideal world, and below are two reasons as for why this is the case:

  • In certain situations, we cannot account for every single case, especially if the number of cases is of a large magnitude

  • In certain situations, we do not have a good solution for solving a problem our program is in

Fortunately, we can utilize the concept of errors to help navigate the flawed world that we live in. More specifically, we can use errors to respect invariants - properties of our programs (i.e. smart contracts) that we wish to maintain throughout its lifetime.

Raising Errors via require

For this section, we refer to Bank which is a smart contract designed to act like a bank:

contract Bank {

    mapping(address => uint256) balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        (bool success, ) = address(msg.sender).call{value: amount}("");
        balances[msg.sender] -= amount;
    }

}

Assuming that deposit is correct, we will focus on the withdraw function; this function is designed to send the caller the desired amount of ether they wish to withdraw. Ideally, we want for withdraw to maintain the following invariants of our Bank contract:

  • Users cannot withdraw more than what they have in the bank

  • The bank successfully sends users their ether

As it currently stands, withdraw does not respect the invariants listed above. However, we can utilize errors to revert the execution of withdraw if either invariant is not respected. In particular, we will utilize the require keyword to maintain both invariants.

Focusing on the first invariant, we want to check that the user is not withdrawing more than what they have in the bank. In terms of pseudocode, we want the following boolean condition to hold:

balance of caller >= amount requested to be withdrawn

The require keyword takes a boolean condition e; if e evaluates to true, nothing happens. Otherwise, if e evaluates to false, the transaction reverts. Therefore, we want to incorporate the following code into our contract:

require(balances[msg.sender] >= amount);

Furthermore, the require keyword also accepts an optional error message. Updating our code to include an error message, we have the following updated contract:

contract Bank {

    mapping(address => uint256) balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Not enough funds!");
        (bool success, ) = address(msg.sender).call{value: amount}("");
        balances[msg.sender] -= amount;
    }

}
contract Bank {

    mapping(address => uint256) balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Not enough funds!");
        (bool success, ) = address(msg.sender).call{value: amount}("");
        require(success, "Sending ether failed");
        balances[msg.sender] -= amount;
    }

}

revert and assert

In addition to require, we also have the keywords revert and assert which also allow us to raise errors in our smart contracts.

revert acts very similarly to require, except that no boolean condition is needed for revert. An example of this can be seen below (the error message is optional, but recommended):

function failingFunction() public {
    revert("Failing just cause");
}

The last keyword we will discuss is assert. assert only takes in a boolean condition. However, what makes assert special is that if it is ever called by a smart contract, all the gas remaining in said transaction is utilized! Whereas for require and revert refund any remaining gas.

PreviousStringsNextEvents

Last updated 1 year ago

At this point, withdraw now respects the first invariant; but what about the second invariant? Recall from the section that the success variable in withdraw indicates whether if the payment of ether was successful. Therefore, we just need to check that success is equal to true. Updating our Bank smart contract one final time, we have:

contract interactions