Solidity Behavioral Patterns : Guard Checks

Aditya BondeAditya Bonde
4 min read

When we build smart contracts, it’s essential to make sure they behave as expected. We can’t have anyone sending bad data or messing with the contract's state. That’s where the Guard Check pattern comes in. Think of it as the contract's way of saying, “Hold on, let’s make sure everything’s right before we move forward.”

In this post, we'll break down the Guard Check pattern, why it's crucial for Solidity developers, and how to implement it effectively.

Why Guard Checks Matter

In real-world contracts, like a will, certain conditions must be met before it goes into effect (e.g., paying out inheritance only after a person’s passing). Smart contracts follow a similar logic, but instead of lawyers, we rely on Guard Checks to verify that the right conditions are met before any transaction happens.

In Solidity, guard checks help us:

  1. Validate user inputs.

  2. Check the contract’s state before any critical operations.

  3. Ensure everything stays consistent (no surprises in our data).

  4. Prevent anything unexpected from sneaking into our contract's logic.

By using guard checks, we can make our code safer and prevent errors. If anything fails a check, we want our contract to revert, undoing all changes and keeping the state intact.

How Solidity Helps Us Implement Guard Checks

Solidity provides three main tools to help us write guard checks: require, revert, and assert. Let’s go over each one and when to use it.

  1. require():

    • Used to validate inputs or the contract’s current state.

    • Ideal for early checks in a function to ensure all conditions are right.

    • Example: Ensure the sender has enough funds to execute a transfer.

    • If require() fails, the transaction reverts, and the unused gas is refunded.

  2. assert():

    • Used to catch serious internal errors and validate conditions that should never fail.

    • Should be placed in parts of the code where any failure indicates a bug.

    • Example: Confirming that the contract’s balance updates correctly after a transfer.

    • If assert() fails, it consumes all remaining gas, making it clear that something critical went wrong.

  3. revert():

    • Used to handle more complex conditions, especially within if-else statements.

    • Example: If multiple conditions must be met, and one fails, we can call revert() with an error message.

    • Like require(), it reverts the transaction and refunds the gas.

Simple Example: Donation Smart Contract

Here’s a fictional example to show how guard checks work in a contract that accepts donations and sends them to charity.

pragma solidity ^0.8.0;

contract GuardCheck {

    function donate(address charity) public payable {
        require(charity != address(0), "Invalid charity address.");
        require(msg.value > 0, "Donation must be greater than zero.");

        uint initialBalance = address(this).balance;
        uint donationAmount;

        if (charity.balance == 0) {
            donationAmount = msg.value;
        } else if (charity.balance < msg.sender.balance) {
            donationAmount = msg.value / 2;
        } else {
            revert("Charity has sufficient funds.");
        }

        (bool sent, ) = charity.call{value: donationAmount}("");
        require(sent, "Failed to send donation.");

        assert(address(this).balance == initialBalance - donationAmount);
    }
}

Breakdown of the Guard Checks

  1. require(charity != address(0), ...): This ensures the charity address isn’t blank, preventing funds from accidentally being sent nowhere.

  2. require(msg.value > 0, ...): This checks that the donation is above zero.

  3. revert("Charity has sufficient funds."): If the charity already has enough funds, the contract reverts with an error.

  4. assert(address(this).balance == initialBalance - donationAmount): Ensures our balance calculation is correct after the donation is sent.

With these checks, we ensure that only valid donations are processed and that any issues are caught and handled immediately.

Benefits of Guard Checks

Guard checks make the code easier to read and understand. require() and assert() offer quick insights into expected conditions, which makes our contract's logic clear even for those new to coding. They also reduce the risk of unintended outcomes or costly gas losses.

However, if guard checks aren’t used carefully, they could lead to unnecessary gas consumption or unexpected reverts. Always make sure to choose the right check based on the scenario.

By implementing the Guard Check pattern, your smart contracts will be more robust, secure, and reliable. It’s a small step with a huge payoff—keeping your code safe and making sure only valid transactions go through. Stay tuned for the next post in this series, where we’ll dive into access control and how to make sure only authorized users can interact with certain parts of your contract!

0
Subscribe to my newsletter

Read articles from Aditya Bonde directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aditya Bonde
Aditya Bonde