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;
    }

}

At this point, withdraw now respects the first invariant; but what about the second invariant? Recall from the contract interactions 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 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.

Last updated