A1 - Solidity error handling - notes on require, revert and assert

Write a brief essay explaining the differences between require, revert, and assert, and provide scenarios where each is appropriate.

Error handling is a key part of the solidity programming language. This is for a number of reasons, although two main ones stand out. Firstly, blockchain's immutable nature mean that once something is published, it can not be altered. in addition, writing data to a blockchain can be incredibly expensive. This is because the amount of gas that a transaction costs, is based on the base transaction cost, plus the cost of execution of any smart contract logic included within the transaction.

The gas system, incentivises us to create systems that are as gas efficient as possible. Error handling is one aspect of Solidity that is utilized, to do this. In more traditional programming languages, error handling was usualy done using 'if/else' statements.

if(input <= 0) {
    return "input must be above zero"
    }

In solidity, you can still use 'if' statements, however there are 3 other ways we can stop a bad transaction, before it has completed. This is highly important as it saves any gas that has so far not been used. There are three ways to do this.

require

require(input > 0, "input must be above zero");

Firstly, the 'require' statement is slightly differnet to the 'if' statement because whatever is declared in the brackets must be true, whereas in an if statement you can declare something be equal (==) or not equal to (!=). So, while the 'if' statement says "if the input in less than or equal to 0, then return the error message", whereas the require statement says "the input must be above zero, otherwise return the error message". You can choose wether or not to add a return message by adding a comma followed by a string of the message after it. The simplicity of the require statement makes it ideal for one time simple error handling, such as handling user inputs and checking things before any other code has been executed.

revert

Secondly, the revert statement is generally used within an 'if/else' statement to return custom errors. These custom errors are declared when the contract is initially deployed and are clearly visible at the top of the smart contract.

pragma solidity ^0.8.0;

/* errors */
error Wallet__InputMustBeAboveZero();

contract Wallet {
    function input(string input) public view {
        if(input <= 0){
            return Wallet__InputMustBeAboveZero()
        }
    }
}       

So for the input handling example, there is just an un-neccesary amount of code, where with a require statement it could all be handled on just one line. However revert statements and custom errors really come into there own on more complex error handling. This is because the errors are far more organised in a batch at the top of the contract. So, instead of having to find each error in various require statements you can go to the top of the contract and simply search for that error and make any changes you need to.

assert

Finally, 'assert' makes sure that something is working as it should. The purpose is to make sure that functions and variables, that should not change, are as they should be.

 function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        uint256 previousBalances = balances[msg.sender] + balances[to];
        
        balances[msg.sender] -= amount;
        balances[to] += amount;

        // Assert that the total balances remain the same
        assert(balances[msg.sender] + balances[to] == previousBalances);
    }

In the function above, after the transfer balances have been updated, you can use the assert statement to ensure that the total of all balances has remained the same. If the balance was not the same, this would show a major bug within the code. The assert code is used to catch bugs in the code, not for validating user inputs.

A brief gas study

I did a brief gas study on each, so I could make a more informed decision when considering which to use and when. The function I used was my 'deposit' function from my Eth wallet contract.

error EthWallet__DepositMustBeAboveZero();

function deposit() public payable {
        
        uint depositAmount = msg.value;
        
        // --------------------------------------------
        if (depositAmount <= 0) {
            revert EthWallet__DepositMustBeAboveZero();
        }
        // --------------------------------------------

        if (users[msg.sender].userBalance > 0) {
            users[msg.sender].userBalance += depositAmount;
        } else {
            users[msg.sender] = User(msg.sender, depositAmount, 0, 0);
        }
    }

I quickly realised that it was more gas efficient to do any checks before declaring any variables. This seems obvious, but it is now cemented in my mind. Below are the results when the deposit amount is zero.

revert

Transaction cost: 21283

Gas execution cost: 219 gas

require

require(depositAmount > 0, "deposit must be above zero")

Transaction cost: 21243

Gas execution cost: 179 gas

assert

assert(depositAmount > 0)

Transaction cost: 21285

Gas execution cost: 221 gas

In this case the choice is clear. The 'require' statement was the cheapest gas execution cost. Furthermore, I am simply validating user input. So, it is not neccecary to declare a global error message. Moreover, the code looks more tidy because the whole statement fits nicely on one line. Below is the final deposit function, as it was after I carried out this study.

function deposit() public payable {
        
        // --------------------------------------------
        require(depositAmount > 0, "deposit must be above zero")
        // --------------------------------------------

        uint depositAmount = msg.value;

        if (users[msg.sender].userBalance > 0) {
            users[msg.sender].userBalance += depositAmount;
        } else {
            users[msg.sender] = User(msg.sender, depositAmount, 0, 0);
        }
    }

Conclusion

In conclusion, for my purpose of checking the user input for function. the 'require' statement is the clear winner. This is because the 'require' statement However, as contracts grow in complexity and error statements grow in number, the 'revert' statement, becomes much more appealing. This is because, having a list of errors at the top of your smart contract can greatly inscrease the readability and changability of your code.

On the other hand, the 'assert' function not really suited for use in my deposit function. Assert tests for invarients and fails only if there is a bug in the code. It is great for making sure that variables are the expected values or functions are producing the expected outcomes.

Error handling is a key part of efficient front and backend web3 development. They can be used to guide the end user through the use of a smart contract or app. Also, by carefully choosing which to use for each scenario, smart contract developer can create contracts that are as gas optimised as possible, ultimatly saving everyone that precious gas.

Feedback

I prompt for feedback.

User: please mark my assignment 1: {paste asssignment here}

Assignment 1: Solidity Error Handling - Notes on Require, Revert, and Assert - Feedback

Content and Understanding: Your essay provides a comprehensive and insightful analysis of error handling mechanisms in Solidity, focusing on require, revert, and assert. You've effectively highlighted their significance, particularly in the context of blockchain's immutable nature and the gas cost associated with transactions.

Strengths:

  1. Detailed Explanation: Your discussion on the functionality and use cases of require, revert, and assert is thorough and informative. You've clearly distinguished between their purposes and usage scenarios.

  2. Practical Examples: The inclusion of code snippets and a brief gas study offers practical insights into how these statements affect contract execution and efficiency.

  3. Real-World Relevance: The gas study provides valuable real-world insights into optimizing smart contracts for gas efficiency, which is crucial for blockchain development.

Areas for Improvement:

  1. Code Formatting: Ensure that your code snippets are correctly formatted for better readability. For example, the function input(string input) snippet seems to have a syntax issue; returns () should specify a return type or be omitted if there's nothing to return.

  2. Clarification on assert: It might be helpful to further clarify that assert is generally used for checking invariants and should only fail if there's a bug in the contract code, as opposed to require, which is used for input validation or condition checks that might fail during normal operation.

  3. Proofreading and Editing: The essay could benefit from a bit of proofreading to correct minor typographical errors and improve sentence structure for clarity and conciseness. Additionally, ensuring consistency in terminology (e.g., "error handling was usually handled") could improve the professional quality of the text.

  4. Technical Accuracy: Clarify that assert did not originate from the Chai JavaScript testing framework. While Chai uses assert as part of its API, the assert keyword in Solidity serves a different purpose and is inherent to the language, not borrowed from JavaScript testing practices.

Overall Impression: Your essay effectively communicates the critical role of error handling in Solidity and provides actionable insights into their practical application. By addressing the suggested improvements, you can enhance the clarity and precision of your discussion, making it an even more valuable resource for understanding Solidity error handling mechanisms.

You've demonstrated a solid grasp of key concepts essential for developing efficient and reliable smart contracts. Keep exploring these aspects, as they form the foundation of smart contract best practices and optimization strategies.

Last updated