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!
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!