Smart Contract Fork Testing Using Foundry Cheatcodes
Published: 2024-06-16 04:42:16


## Introduction
Testing is important in smart contract development due to the immutable nature of smart contracts. Testing helps identify and resolve potential security vulnerabilities in smart contracts. Safeguard against unauthorized access.

Sometimes smart contract developers must interact with real-world data that testnet cannot provide. Hence, there is a need for fork testing. In this article, readers will learn how to conduct fork-testing in a foundry development environment.
 
## Content
1. Introduction
2. Prerequisites
3. Benefits of fork testing?
4. What are Foundry Cheatcodes?
5. Project Setup and Testing
6. Conclusion.

## Prerequisites
This tutorial requires foundry installation. 
Knowledge of Solidity programming and smart contract development.

## Benefits of fork-testing
Fork testing mimics the production environment as much as possible. There is no need to use a testnet public faucet to get test coins for testing. 
- It allows developers to debug in an environment that is as close to the production as possible.
- It gives developers access to real-time data such as the current state of the blockchain which testnet cannot provide since testnet operates in an isolated environment.
- It gives developers unprecedented control over smart contracts. Developers can mint or transfer tokens like they own the token creation smart contract.
- Developers can create blockchain addresses easily.

## What are Foundry Cheatcodes?
According to Foundry Documentation, Forking cheatcodes allow developers to fork blockchain programmatically using solidity instead of CLI arguments. Forking cheat codes support multiple forks and each fork has a unique `uint256` identifier. The developer must assign a unique identifier when the developer creates the fork. Forking cheatcodes execute each test in its own standalone `EVM`. Forking cheatcodes isolate tests from one another, and execute tests after the setUp function. It implies that a test in forking cheatcodes mode must have a setup function.

## Project setup and testing
To demonstrate fork testing, we will create a savings smart contract. The contract will allow users to save, set a deadline for withdrawal, and allows users to withdraw when the deadline has elapsed.
 Open a CLI terminal and run the command below to scaffold a foundry project. Name the project `fork_testing`.
`forge init fork_testing`.
Navigate to the src folder create a file and name it `Savings.sol`.
 Open the `Savings.sol` file and create a Savings contract as shown below.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13; 

contract Savings {

 }
```
 Create the under-listed variables and an event in the contract as shown below.
```solidity
event Saver(address payer, uint256 amount);
mapping(address => uint256) public balances;
mapping(address => uint256) public tenor;
bool public openForWithdraw = false;
uint256 public contractBalance = address(this).balance;
```
The variable  `tenor` maps the address of the saver to the duration in seconds the user wants to keep his token. The variable balance maps the address of a saver to the amount saved in the contract. The boolean variable `openForWithdraw` is set to false and does not allow the saver to withdraw before the tenor lapses. The contract will emit the Saver event when a user transfers or sends funds to the contract.

Next, add a `receive () payable` function that allows the contract to receive funds. The function will require the user to set a tenor before the user can send funds to the contract. The function will use the map balances to keep track of the funds sent by an address. On successful savings, the function emits the address and amount sent by a user. The `receive()` is as shown in the code below.
```solidity
receive() external payable {
        require(tenor[msg.sender] > 0, "You must set a tenor before saving");
        balances[msg.sender] += msg.value;
        contractBalance = address(this).balance;
        emit Saver(msg.sender, msg.value);
    }

```
Next add two functions to the contract, a `setTenor()` and a `getTenor()` view function. The `setTenor()` allows the user to set the duration the user wants to keep the funds and `getTenor()` retrieves the duration the user wants to keep the funds in the contract. The implementation of the functions is shown below.
```solidity
function setTenor(address saver, uint256 _tenor) public {
        tenor[saver] = block.timestamp + _tenor;
    }

function getTenor(address saver) public view returns (uint256) {
        return tenor[saver];
    }
```
Add a get balance() as shown below. The function returns the total funds received by the contract.
```solidity
function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
```
Next, add a view function `getIndividualBalance()`  as shown below. The `getIndividualBalance()` returns the balance of the individual address.
```solidity
function getIndividualBalances(
        address saver
    ) public view returns (uint256) {
        return balances[saver];
    }
```
Add a `timeLeft()` view function that returns the time left before the tenor lapses. The implementation is shown below.
```solidity
function timeLeft(address saver) public view returns (uint256) {
        if (block.timestamp >= tenor[saver]) {
            return 0;
        } else {
            return tenor[saver] - block.timestamp;
        }
    }
```
Lastly add a withdraw function, that allows the user to withdraw their funds if the tenor has elapsed. The implementation is shown below.
```solidity
 function withdraw(uint amount, address withdrawer) public {
        if (timeLeft(withdrawer) <= 0) {
            openForWithdraw = true;
        }
        require(openForWithdraw, "It is not yet time to withdraw");
        require(
            balances[withdrawer] >= amount,
            "Balance less than amount to withdraw"
        );
        balances[withdrawer] -= amount;
        (bool success, ) = withdrawer.call{value: amount}("");
        require(success, "Unable to withdraw fund");
    }
```
Below is the full code implementation of the `Savings` contract.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Savings {
    event Saver(address payer, uint256 amount);
    mapping(address => uint256) public balances;
    mapping(address => uint256) public tenor;
    bool public openForWithdraw = false;
    uint256 public contractBalance = address(this).balance;

    // Collect funds in a payable `receive()` function and track individual `balances` with a mapping:
    // Add a `Saver(address,uint256, uint256, uint256)`
    receive() external payable {
        require(tenor[msg.sender] > 0, "You must set a tenor before saving");
        balances[msg.sender] += msg.value;
        contractBalance = address(this).balance;
        emit Saver(msg.sender, msg.value);
    }

    // Set the duration of time a user will save token in the contract.
    function setTenor(address saver, uint256 _tenor) public {
        tenor[saver] = block.timestamp + _tenor;
    }

    // Returns the duration of time a user is willing save funds in the contract.
    function getTenor(address saver) public view returns (uint256) {
        return tenor[saver];
    }

    // Returns the contract balance.
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // Returns the balance saved in the contact by an address.
    function getIndividualBalances(
        address saver
    ) public view returns (uint256) {
        return balances[saver];
    }

    // Returns the time left before the tenor elapsed.
    function timeLeft(address saver) public view returns (uint256) {
        if (block.timestamp >= tenor[saver]) {
            return 0;
        } else {
            return tenor[saver] - block.timestamp;
        }
    }
    // Allows a user to withraw funds once the tenor has elapsed.
    function withdraw(uint amount, address withdrawer) public {
        if (timeLeft(withdrawer) <= 0) {
            openForWithdraw = true;
        }
        require(openForWithdraw, "It is not yet time to withdraw");
        require(
            balances[withdrawer] >= amount,
            "Balance less than amount to withdraw"
        );
        balances[withdrawer] -= amount;
        (bool success, ) = withdrawer.call{value: amount}("");
        require(success, "Unable to withdraw fund");
    }
}
```
### Testing the Savings contract.
Open the test folder, create a `.env` file, and add a variable `MAINET_RPC_URL`. Copy and paste your mainnet RPC_URL as value as shown below.
` MAINET_RPC_URL = ‘https://your_rpc_url’ `
Next, create a `Savings.t.sol` file. Open the file and specify the license and solidity compiler version. In the file, import the foundry Forge Standard Library, the `Savings` contract and forge standard output as shown below.  Forge Standard Library is a collection of contracts that makes Test contracts easy to write and test. The `forge-std/Test.sol` contains the forge standard experience. 
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

//import dependencies
import "forge-std/Test.sol";
import "../src/Savings.sol";
import "forge-std/console.sol";
```
Add a test contract to the Savings.t.sol file. Let the test contract inherit from the `Test.sol`.
```solidity
contract TestSavings is Test {
    
}
```
Add the variables listed below to the contract.
```solidity
uint256 mainnetFork;
string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
Savings public savings;
```
The variable `mainnetFork` will hold the fork's unique identifier, and the string `MAINNET_RPC_URL ` holds the RPC_URL loaded from the .env file using cheatcode`vm.envString(). The vm is an instance of `forge `cheatcodes.`

Next,  add `receive() external payable{}` to allow the test contract to receive funds. Add a `setUp` function to the contract, to create, select the mainnet fork and create an instance of the `Savings` contract as shown below.
```solidity
 function setUp() public {
        mainnetFork = vm.createFork(MAINNET_RPC_URL);
        vm.selectFork(mainnetFork);
        savings = new Savings();
    }
```
In the code above, we used the cheatcodes `vm.createFork(MAINNET_RPC_URL)` to fork the Ethereum mainnet blockchain and create a unique uint256 identifier which we assigned to the variable `mainnetFork`. Next, we select the fork with `vm.selectFork(mainnetFork)`.

Since forge cheatcodes isolate tests from one another, we will create six test functions listed below.
- `function testInitialBalance() public view {}`: To test if the Savings contract initial balance is zero.
- `function testSavingsWithoutTenor() public{}`: To test if a user can send funds to the Savings contract without a tenor. The test should revert if a user did not set a tenor before sending funds to the Savings contract.
- `function testSetTenor() public {}`: The test expects the tenor to be greater than the current `block.timestamp`.
- `function testSavings() public{}`: Once, the user sets a tenor, the test expects the user to be able to save.
- `function testWithdrawBeforeTime() public {}`: The user should not be able to withdraw funds before the tenor elapses.
- `function testWithdrawAfterTime() public {}`: The user should be able to withdraw funds after the tenor elapses.
The full code implementation of the test contract is shown below.
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

//import dependencies
import "forge-std/Test.sol";
import "../src/Savings.sol";
import "forge-std/console.sol";

//Test Contract
contract TestSavings is Test {
    uint256 mainnetFork;
    string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
    Savings public savings;

    // Allow the test contract to receive funds.
    receive() external payable {}

    // Initial configuration.
    function setUp() public {
        mainnetFork = vm.createFork(MAINNET_RPC_URL);
        vm.selectFork(mainnetFork);
        savings = new Savings();
    }

    // The initial balance should be 0.
    function testInitialBalance() public view {
        assertLt(savings.getBalance(), 1);
    }

    // User should not be able to save without setting a tenor.
    function testSavingsWithoutTenor() public {
        vm.deal(address(this), 2 ether);
        vm.expectRevert();
        (bool sent, ) = address(savings).call{value: 1 ether}("");
        require(sent, "Failed to send Ether");
    }

    // User should be able to set tenor.
    function testSetTenor() public {
        savings.setTenor(address(this), 30);
        uint tenor = savings.getTenor(address(this));
        assertGt(tenor, block.timestamp);
    }

    // User should be able to save, if the user has set a tenor.
    function testSavings() public {
        savings.setTenor(address(this), 30);
        vm.deal(address(this), 2 ether);
        (bool sent, ) = address(savings).call{value: 1 ether}("");
        require(sent, "Failed to send Ether");
        assertGt(savings.getIndividualBalances(address(this)), 0);
    }

    // User should not be able to with the tenor elapses.
    function testWithdrawBeforeTime() public {
        savings.setTenor(address(this), 30);
        vm.deal(address(this), 2 ether);
        (bool sent, ) = address(savings).call{value: 1 ether}("");
        console.log(sent);
        vm.expectRevert("It is not yet time to withdraw");
        savings.withdraw(0.5 ether, address(this));
    }

    // User should be able to withdraw after the tenor elapses.
    function testWithdrawAfterTime() public {
        savings.setTenor(address(this), 0);
        vm.deal(address(this), 1 ether);
        (bool sent, ) = address(savings).call{value: 1 ether}("");
        console.log(sent);
        uint256 oldBalance = address(this).balance;
        savings.withdraw(0.5 ether, address(this));
        uint256 newBalance = address(this).balance;
        assertGt(newBalance, oldBalance);
    }
}
```
In the  `testBalance()` , we use the `assertLt()` to check if the contract balance is less than 1. The test should pass. 

The `testSavingsWithoutTenor()` test if a user can save without setting a tenor. In the function, we retrieve the balance of the Savings contract with `savings.getBalance()` and assign the return value to the variable initial balance. Then we set the balance of the test contract to `2 ether` using `vm.deal()`. We expect that when a user tries to send funds to the Savings contract without a tenor, the transaction should revert. We use `vm.expectRevert()` to handle the revert. Then, we send `1 ether` to the Savings contract from the test contract using ` (bool sent, ) = address(savings).call{value: 1 ether}("")`. Next, we retrieve the balance of the Savings contract and assign the return value to the variable `newBalance`. Then, we test if the new balance of the savings contract is equal to the initial balance using `assertEq()`. We expect the test to pass.

The function `testSetTenor` tests if a user can set the tenor for funds to be stored in the savings contract. We set the tenor using `setTenor()` of the Savings contract with the address of the test contract and the duration of 30 seconds. The test expects the tenor should be greater than the current `block.timestamp`.

The  `testSavingsWithTenor()` tests if a user can save after the user sets a tenor. We set the tenor with the `savings.setTenor` to 30 seconds, set the balance of the test contract to `2 ether`, then transfer `1 ether` to the  `Savings` contract. We retrieve the balance mapped to the test contract address in the `Savings` contract with the `getIndividualBalances` function. We expect the retrieved balance to be greater than 0.

The `testWithdrawBeforeTime()` tests if a user can withdraw funds before the tenor elapses. We send `1 ether` to the savings contract, then try to withdraw `0.5 ether` from the `Savings` contract to the test contract with `savings.withdraw(0.5 ether, address(this))`. We expect the transaction to revert.  

The `testWithdrawAfterTime()` tests if a user can withdraw after the tenor has elapsed. We set the tenor to `0 seconds`, retrieve the balance of the test contract and assign it to the variable oldBalance, then transfer `1 ether` to the `Savings` contract,  and withdraw `0.5 ether` from the `Savings` contract.  Thereafter, retrieve the balance of the test contract and assign the value to the variable newBalance. We expect that the newBalance should be greater than the oldBalance.

Run the tests with the command:
`forge test`
For trace result, run the command:
`forge test -vvv`
Or
`forge test -vvvv`

## Conclusion
Testing is essential in smart contract development, it enables developers to discover errors, and security vulnerabilities in smart contracts before deployment. Once we deploy a smart contract, it becomes immutable. Fork testing is a way to test smart contracts in an environment that is as close to the production environment as possible. In this article, we have demonstrated how to carry out fork testing using Foundry cheatcodes. We have demonstrated how it is easy to fork an Ethereum mainnet and transfer `ether` from one contract to another without the need to use testnet faucet or tokens.


Like, Share, or Comment if you find this article interesting.