Setting Up a Project with Foundry

Setting Up a Project with Foundry

A beginner guide to deployments and tests

·

7 min read

Introduction

This guide covers the process of setting up, compiling, deploying, and interacting with smart contracts using Foundry, a powerful development environment for Ethereum smart contracts.

1. Initializing a Project

Creating a New Project

  • In an empty directory, initialize a new Foundry project:

      forge init
    
  • To create a new directory with the project:

      forge init PROJECT_NAME
    
  • Note: The src directory is where the smart contracts are placed.

2. Compiling Contracts

  • To compile the contracts, run either:

      forge build
    

    or

      forge compile
    
  • The out directory will generate a JSON file containing compilation data, such as ABI.

3. Setting Up a Local Blockchain

  • Use Anvil to start a local blockchain for testing:

      anvil
    
  • The local blockchain will run at 127.0.0.1:8545, and you can add it to MetaMask for ease of testing.

4. Deploying Contracts

Deploying Locally or to a Custom RPC

  • To deploy smart contracts, use forge create. Forge defaults to the Anvil local blockchain, but other RPCs can be specified using the --rpc-url flag.
💡
Get a free RPC from Chainstack.
  • Deploying locally with Anvil running:

      forge create CONTRACT_NAME
    
  • Deploying to a custom endpoint:

      forge create CONTRACT_NAME --rpc-url YOUR_ENDPOINT
    
  • This will likely not work because it needs a private key to deploy.

Error:
Error accessing local wallet. Did you set a private key, mnemonic or keystore?
Run `cast send --help` or `forge create --help` and use the corresponding CLI
flag to set your key via:
--private-key, --mnemonic-path, --aws, --interactive, --trezor or --ledger.
Alternatively, if you're using a local node with unlocked accounts,
use the --unlocked flag and either set the `ETH_FROM` environment variable to the address
of the unlocked account you want to use, or provide the --from flag with the address directly.

Options for Specifying Private Key

  • Run the create command with the --interactive flag for a prompt to add a private key:

      forge create CONTRACT_NAME --interactive
    
  • Or, directly include the private key in the command:

      forge create CONTRACT_NAME --private-key YOUR_PRIVATE_KEY
    

5. Writing Deploy Scripts

  • Scripts in Foundry are written in Solidity. We'll use a Solidity script to deploy a contract. By convention, script files end with .s.sol.

  • Example: Deploying SimpleStorage.sol.

Creating the Deploy Script

  • Create a file named deploySimpleStorage.s.sol with the following content:

      // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.19;
    
      import {Script} from "forge-std/Script.sol";
      import {SimpleStorage} from "../src/SimpleStorage.sol";
    
      contract DeploySimpleStorage is Script {
          function run() external returns (SimpleStorage) {
              vm.startBroadcast();
              SimpleStorage simpleStorage = new SimpleStorage();
              vm.stopBroadcast();
              return simpleStorage;
          }
      }
    
  • Deploy the contract using the script:

      forge script script/DeploySimpleStorage.s.sol --rpc-url YOUR_RPC --broadcast --private-key YOUR_PRIVATE_KEY
    

Let's break down its key components and functionalities:

  1. Pragma Directive:

    • pragma solidity ^0.8.19;: Specifies that the script is compatible with Solidity version 0.8.19 or any newer version of the 0.8 series but not version 0.9 or above.
  2. Imports:

    • import {Script} from "forge-std/Script.sol";: Imports the Script class from the forge-std library, which is a part of Foundry, a development environment for Ethereum smart contracts.

    • import {SimpleStorage} from "../src/SimpleStorage.sol";: Imports the SimpleStorage contract, presumably a custom contract located in the src directory.

  3. Contract Declaration:

    • contract DeploySimpleStorage is Script: Defines a new contract named DeploySimpleStorage that inherits from the Script class. This setup is typical for deployment scripts in Foundry.
  4. Function Definition:

    • function run() external returns (SimpleStorage): The run function is the main entry point for the deployment script. It's marked external as it's intended to be called externally, and it returns an instance of SimpleStorage.
  5. Deployment Process:

    • vm.startBroadcast();: Initiates a transaction broadcast. The vm object is a special component in Foundry, providing various functionalities related to the Ethereum Virtual Machine (EVM).

    • SimpleStorage simpleStorage = new SimpleStorage();: Instantiates the SimpleStorage contract.

    • vm.stopBroadcast();: Ends the transaction broadcast.

  6. Return Statement:

    • return simpleStorage;: Returns the deployed instance of SimpleStorage.

This script is a typical example of a deployment script used in the Foundry environment for deploying Ethereum smart contracts. It's concise and follows the pattern of starting a broadcast, deploying the contract, and stopping it. The SimpleStorage contract, which is not detailed here, would contain the actual business logic or data storage mechanisms.

6. Interacting with Contracts Using Cast

Sending Transactions

  • To send transactions, use cast send:

      cast send ADDRESS FUNCTION_SIG PARAMS
    
  • Example:

      cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "store(uint256)" 3333 --rpc-url $RPC_URL --private-key $PRIVATE_KEY
    

Reading from Contracts

  • Use cast call for reading view functions:

      cast call ADDRESS FUNCTION_SIGNATURE
    
  • Example:

      cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "retrieve()"
    

You can use cast for conversions, for example, hex to dec:

cast --to-base 0x0000000000000000000000000000000000000000000000000000000000000d05 dec

Or use the Chainstack EVM Swiss Knife.

7. Managing Dependencies

Installing Smart Contract Dependencies

  • Use the following command to install dependencies from a repository:

      forge install smartcontractkit/chainlink-brownie-contracts --no-commit
    
  • Dependencies are added to the lib directory.

Remapping Dependencies

  • Add remappings in the foundry.toml file for syntax convenience:

      remappings = ['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/']
    

chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/']

8. Writing and Running Tests

Example Test Contract

  • Tests in Foundry are also written in Solidity. Here's an example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {Test, console} from "forge-std/Test.sol";

contract FundMeTest is Test {
    uint256 number = 33;

    function setUp() external {
        number = 3333;
    }

    function testDemo() public {
        console.log("The saved number is", number);
        assertEq(number, 3333);
    }
}
  • Run tests with:

      forge test -vv
    
  • The -vv flag outputs detailed logs for better insight.

Let's break down its components:

  1. License and Solidity Version Declaration:

    • // SPDX-License-Identifier: MIT: This is a comment specifying the license under which this file is released, in this case, the MIT License.

    • pragma solidity ^0.8.18;: This line specifies the compiler version. The file is compatible with Solidity version 0.8.18 and above within the 0.8.x range.

  2. Imports:

    • import {Test, console} from "forge-std/Test.sol";: This line imports two elements from the Forge standard library (forge-std):

      • Test: A base contract that provides testing functionalities.

      • console: A utility to log output to the console. This is particularly useful for debugging and tracking variable values during test execution.

  3. Test Contract Declaration:

    • contract FundMeTest is Test {: This line declares a new contract FundMeTest which inherits from the Test contract. In the context of Forge, this means FundMeTest is a test suite.
  4. State Variable:

    • uint256 number = 33;: A state variable number of type uint256 (unsigned integer of 256 bits) is declared and initialized to 33. This variable is used to demonstrate state manipulation and assertion in the test.
  5. Setup Function:

    • function setUp() external { number = 3333; }: The setUp() function is a special function in the Forge framework that runs before each test function. It's used for initializing or resetting the state. Here, it sets the number variable to 3333.
  6. Test Function:

    • function testDemo() public { ... }: This is the actual test function. In Forge, any function with a name starting with test is considered a test case.

      • console.log("The saved number is", number);: This line logs the value of number to the console, which is useful for debugging or verifying the test state.

      • assertEq(number, 3333);: This is an assertion statement provided by the Test contract. It checks whether the value of number is equal to 3333. If the assertion fails (i.e., if number is not 3333), the test will fail.

9. Testing on a Fork

  • Run tests on a forked network by adding an RPC URL:

      forge test -vvv --fork-url $SEPOLIA_RPC
    

10. Coverage Analysis

  • Use forge coverage to analyze how much of your contracts are tested:

      forge coverage --fork-url $SEPOLIA_RPC
    

    It will display a nice table:

[⠢] Compiling...
[⠢] Compiling 26 files with 0.8.20
[⠆] Solc 0.8.20 finished in 4.07s
Compiler run successful!
Analysing contracts...
Running tests...
| File                      | % Lines       | % Statements  | % Branches    | % Funcs      |
|---------------------------|---------------|---------------|---------------|--------------|
| script/DeployFundme.s.sol | 0.00% (0/3)   | 0.00% (0/3)   | 100.00% (0/0) | 0.00% (0/1)  |
| src/FundMe.sol            | 16.67% (2/12) | 23.53% (4/17) | 0.00% (0/4)   | 25.00% (1/4) |
| src/PriceConverter.sol    | 0.00% (0/6)   | 0.00% (0/11)  | 100.00% (0/0) | 0.00% (0/2)  |
| Total                     | 9.52% (2/21)  | 12.90% (4/31) | 0.00% (0/4)   | 14.29% (1/7) |

Did you find this article valuable?

Support Davide by becoming a sponsor. Any amount is appreciated!