How to test Smart Contracts on the Mainnet (Ganache)

The end goal of an Ethereum project is to end up on the Mainnet, right? But we have to find out how it would behave in that crazy jungle before deploying it. Let’s see how to test our smart contracts on the mainnet.

The only way to get as close as possible to the mainnet is by forking it i.e. creating our own copy of the network. Reasons for doing that:

  • See how your contracts behave against the real state of the blockchain
  • Interact with contracts only deployed on the mainnet
  • Pretend like getting millions of tokens at your address 🤑

On our Ethereum fork, we will be able to interact with contracts and addresses which were populating the mainnet. Therefore, we can test smart contracts on the mainnet WITHOUT SPENDING ANY ACTUAL ETHER.

This article will explain how to do this step by step:

  1. Introduction & dependencies
  2. Connection to the mainnet
  3. Fork the mainnet
  4. Test your contracts

1. Introduction & dependencies

Our goal is to use the main network of Ethereum while not spending any money. The closest we can get to it is by forking it.

What does forking mean?

Forking means making a copy of a system in order to start independent development on it. In blockchain, a fork is a clone of the network’s activity up until the fork took place. It essentially copies every block registered so far and allows you to have full control over future transactions.

Although, there are some downsides too. With a fork, you cannot really test your contracts against live activity. 

Well, I suppose there’s no such thing as “testing in production” in blockchain. This is why Dapp developers have to be extra careful before deploying. 

But using a fork definitely gives you a better sense of the real behaviour of your Dapp.

Dependencies

For simplicity, we are going to use the Truffle development environment to write our contracts and test them. If you are not familiar with Truffle, I suggest going through this tutorial first: How to work with Truffle and Ganache.

To install Truffle, use npm:

npm install truffle

Or use yarn:

yarn add truffle

Now we need to use the Ganache tool which will be the one helping us fork the mainnet.

Ganache is a great tool for developers. It allows you to test your contracts by providing a local blockchain and a set of addresses funded with test ETH.

To fork the main network, we need to use Ganache CLI as it has more functionalities than the GUI (user interface).

To install it, use npm:

npm install -g ganache-cli

Or yarn:

yarn global add ganache-cli

Setting up the Truffle project

For this tutorial, we will explain how to test your contracts on the mainnet using a simple example that interacts with a deployed token, DAI. The example will be a simple voting game contract that rewards the most voted candidate. 

The contract will have a public vote function that allows anyone to pay 1 DAI in order to vote for “their favourite address”. After some votes are gathered, the owner can distribute the prize to the winner.

Let’s set up this small project using Truffle. Initialise the project through the terminal:

truffle init

Go to the “contracts” folder and add a Vote.sol file:

pragma solidity >=0.4.22 <0.9.0;
import "./IERC20.sol";
contract VoteImport {
  mapping (address => uint) public votes;
  address winner;
  uint public winner_votes;
  address public owner;
  address public DAI_ADDRESS;
  uint public fund;
  modifier onlyOwner() {
      require(msg.sender == owner, "Only owner can use");
      _;
  }
  constructor(address _DAI_ADDRESS) public{
    owner = msg.sender;
    DAI_ADDRESS = _DAI_ADDRESS;
  }
  function vote(address voteFor) public {
    uint amount = 10 ** 18;
    IERC20(DAI_ADDRESS).transferFrom(msg.sender, address(this), amount);
    votes[voteFor] +=1;
    fund += amount;
    if(votes[voteFor] > winner_votes){
      winner_votes = votes[voteFor];
      winner = voteFor;
    }
  }
  function sendPrize() public onlyOwner{
    uint toTranfer = fund;
    fund = 0;
    IERC20(DAI_ADDRESS).transfer(winner,  toTranfer);
  }
}

DAI is an ERC20 token and, to reference it, we used the IERC20 interface and its address. The constructor of the Vote contract gets as parameter the Ethereum address of the DAI token contract.

We use DAI’s transferFrom and transfer functions to manage the transactions between the voter/winner and the contract.

Now compile the code to check if there are no syntax errors:

truffle compile

Now let’s see how we can test that on the main network with the actual DAI token.

2. Connecting to the mainnet

To copy the Ethereum network we first need a connection to it i.e. an endpoint. 

You can certainly build your own node but that takes some effort. So let’s use the Infura API which gives you immediate access to the blockchain.

Go to https://infura.io/ and create a free account. Then go to the Ethereum section and create a new project.

Test smart contracts on the mainnet - Infura

After setting the name of your project, you will be given an Infura key and an endpoint URL. We will need to use those to get access to the Ethereum network.

Make sure the network is set to Mainnet. You can also connect to the test networks, but that’s not the purpose right now.

3. Forking the mainnet

Now let’s get to the fun part.

Go to Infura and copy the endpoint URL you were given. The simplest command version to use is:

ganache-cli --fork https://mainnet.infura.io/v3/{infura_project_id}

There are some more options you can use to customize the fork.

If you wish to fork it from a specific block use:

ganache-cli --fork https://mainnet.infura.io/v3/{infura_project_id}@{block_number}

Often we don’t have many actual funds in our development address. Therefore we need to get some ETH and maybe other tokens from somewhere. 

Because we are forking the mainnet, there’s no such thing as a faucet here. So we need an account with many funds. Fortunately, Ganache allows us to get access to any account after we forked the network.

Go on Etherscan and find an address that owns many ETH (or any other token you need, in our case DAI). Copy it and unlock it within the forking command:

ganache-cli --fork https://mainnet.infura.io/v3/{infura_project_id} --unlock {wealthyAddress}

You can also set a specific network id:

ganache-cli --fork https://mainnet.infura.io/v3/{infura_project_id} --unlock {wealthyAddress} --networkId 999

Great! Now you have an Ethereum fork running at 8565.

Keep it running on one terminal window.

4. Test your contracts

Now let’s see how our contract behaves on the main network’s fork.

Deployment setup

To deploy the Vote contract on the fork we just made, we need to do two more things:

  • Give Truffle the details of the network we are working on (our fork)
  • Write the deployment script for our contract

Go to the truffle-config.js file and add a new network to the networks section:

networks:{
…..
mainfork: {
      host: "127.0.0.1",    
      port: 8545,           
      network_id: "999",    
 },
…...
}

Now we have to write the migration script. Add a .js file in your Truffle project’s migrations folder that has a higher suffix than 1. It should look like this:

const VoteImport = artifacts.require("VoteImport");
module.exports = function (deployer) {
  deployer.deploy(Vote, '0x6B175474E89094C44Da98b954EedeAC495271d0F');
};

When deploying the Vote contract, we need to pass DAI’s address as an argument for the constructor.

Testing

Let’s write a test for the vote function. Create a new testVote.js file in the test folder. 

For dependencies, we will need the Vote contract, the IERC20 interface and  web3 for some utility functions.

const VoteImport = artifacts.require("VoteImport");
const IERC20 = artifacts.require("IERC20");
const Web3 = require("web3");
const web3  = new Web3(`http://localhost:8545`);
const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F';
contract('Vote', (accounts) => {
  let contractInstance = null;
  let wealthyAddress = '0xb527a981e1d415AF696936B3174f2d7aC8D11369';
  let inastanceDAI = null;
  before(async () => {
    contractInstance = await VoteImport.deployed(DAI_ADDRESS);
  });
  it('should vote and send DAI', async () => {
    inastanceDAI = await IERC20.at(DAI_ADDRESS);
    const vote_address = '0x5920d260B90CD01130C65bCF8F15A089B4035CA0';
    let amount = web3.utils.toBN(10**18);
    // wealthyAddress sends 1 DAI and votes for an address
    await inastanceDAI.approve(contractInstance.address, amount, {from: wealthyAddress}); //approve spending
    await contractInstance.vote(vote_address, {from: wealthyAddress});
    // if vote was added
    const votes = await contractInstance.winner_votes();
    assert.equal(votes, 1, "votes not correct");
    // check if 1 DAI was transferred
    let expectedFund = await inastanceDAI.balanceOf(contractInstance.address);
    let actualFund = await contractInstance.fund();
    assert.equal(amount.cmp(expectedFund), 0, "DAI not received");
    assert.equal(amount.cmp(actualFund), 0, "Fund not modified");
  });
});

We will need an instance of the Vote contract and one of the DAI token contract. For the “wealthyAddress” to vote, they first have to approve that the contract can take their DAI. 

Using the assert statement, we check if the vote was registered. Then we verify if the 1 DAI was sent and if the fund variable was updated.

To run the test, first, make sure that the fork network is still working. Then run the command:

truffle test --network mainfork

You should see one test passing.

test smart contracts on mainnet - Tests

Amazing! Now you know how to test your smart contracts on the mainnet. 
I will leave it as an exercise to test the other function, sendPrize.


I hope this was helpful! Leave any suggestions in the comments 😁

Leave a Reply