Build a Smart Contract using Forge

Build a Smart Contract using Forge

Foundry is an Ethereum development framework that allows developers to build projects and test smart contracts. Forge is a tool that ships with Foundry, enabling you to write tests in Solidity.

Written in Rust by developers at Paradigm, Foundry is a blazing-fast alternative to Hardhat and other testing environments.

In this guide, we are going to set up a Solidity project using thirdweb and Foundry, so we are going to want to make sure we have already installed Foundry by following the instructions in the Foundry Book.

Create A New Forge Project

To get started, create a new directory and run the following command

npx thirdweb@latest create contract

Which will prompt you to answer a series of questions to set up your project:

  • Give your project a name
  • Select Forge as the framework
  • Select ERC721 as the base contract
  • Select Drop as an optional extension

Finally, run

forge clean && foundryup && forge update

within your project directory.

After everything is installed, navigate to your project directory, and let's start writing some Solidity!

Setting up the Contract

We'll be writing the contract inside the Contract.sol file within our src folder.

Initially, the code should look like this:

pragma solidity ^0.8.13;

import "@thirdweb-dev/contracts/base/ERC721Drop.sol";

contract Contract is ERC721Drop {
    constructor(
        string memory _name,
        string memory _symbol,
        address _royaltyRecipient,
        uint128 _royaltyBps,
        address _primarySaleRecipient
    )
        ERC721Drop(
            _name,
            _symbol,
            _royaltyRecipient,
            _royaltyBps,
            _primarySaleRecipient
        )
    {}
}

Let's create the function that we'll test: a standard ERC721 mint function. Add the following function to your contract:

function mint(address _to, uint256 _amount) external {
    require(_amount > 0, 'You must mint at least one token!');
    _safeMint(_to, _amount);
}

This function takes in two parameters, _to: the address to which the tokens will be minted, and _amount: the number of tokens to be minted. We've also added a require statement, which checks to ensure that when calling mint, we're minting at least one token.

Writing Tests

Now that our contract is written let's test our mint function using Forge. One of the benefits of Foundry is that as well as writing your contracts in Solidity, you can also test them using Solidity, simplifying the developer workflow.

Inside Contract.t.sol, make sure to have both of the required imports: the contract we are testing and the standard library test contract that should already be included

import "forge-std/Test.sol";
import "../src/Contract.sol";

Next, we'll create a new contract called ContractTest that inherits from the Test contract that's imported from Test.sol. Firstly, we need to create an instance of the Contract within our setUp() function, and we will also need an address to test our contact. We will use a helper function makeAddr from the Forge standard library to do this.

contract ContractTest is Test {
    Contract drop;
    address testAddr = makeAddr("Test");

    function setUp() public {
        drop = new Contract("MintTest", "MT", testAddr, 500, testAddr);
    }  
}

We will now write two tests: testDropWithZeroTokens() to test if the transaction will revert if no tokens are being attempted to be minted and testDropWithTokensMinted() Β to test that the correct number of tokens are minted if the amount minted is more than 0.

function testDropWithZeroTokens() public {
	drop.mint(testAddr, 0);
}

function testDropWithTwoTokens() public {
	drop.mint(testAddr, 2);
}

To check whether the tests pass, run the command

forge test

within the command line.

Oh no, one of our tests has failed! When running the tests, we get an error in the console with one pass and one failed test. When writing tests, it is important to design them so that the tests pass even if we expect a transaction to revert. The testDropWithZeroTokens function will revert as we called mint with zero tokens.

For this test to pass we will use a Forge cheat code: vm.expectRevert(<insert error message>), ensuring that the error message provided as an argument matches the one in the revert statement in our contract. The way this function works is that it checks that the next call reverts with the given error message. Therefore, we will add this line immediately before we call mint , and our tests should now both pass!

Lastly, let's check that the token balance is accurate after calling mint in our test functions. To do this, we can use assertEq(param_1, param_2) which checks that parameter 1 is equal to parameter 2 and returns an error if not. After calling mint, let's add the following to both of our tests

assertEq(drop.balanceOf(testAddr), <expected balance>)

The final test contract Contract.t.sol should look like this:

pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Contract.sol";

contract ContractTest is Test {
    Contract drop;
    
    address testAddr = makeAddr("Test");

    function setUp() public {
        drop = new Contract("MintTest", "MT", testAddr, 500, testAddr);
    }

    function testDropWithZeroTokens() public{
        vm.expectRevert("You must mint at least one token!");
        drop.mint(testAddr, 0);
        assertEq(drop.balanceOf(testAddr), 0);
    }   
    function testDropWithTokensMinted() public{
        drop.mint(testAddr, 2);
        assertEq(drop.balanceOf(testAddr), 2);
    } 
}

Rerun your tests, and they should now both pass!

Both tests passed!

Deploying

Now that we've tested the mint function let's deploy this contract! To do so, run the command

npx thirdweb@latest deploy

and enter the constructor parameters. This will open up your Dashboard, where you can view and manage your contract.

Congratulations on deploying your new forge-tested ERC721 Drop contract! πŸŽ‰

Make sure to jump onto our Discord if you have any questions or want to share your awesome projects with us!