Truffle Migrate: interdependent contracts, libraries, arguments

Depending on your application, Truffle migrations can become quite complex and confusing. The difficult part is to understand how to migrate interdependent contracts with arguments & libraries and how to handle the deployment aftermath.

Truffle provides you with a great framework for deploying your smart contracts. The migrations are a key part of this process and, as it says in the Truffle documentation, they are “responsible for staging your deployment tasks”.

When you only have one contract to deploy, the migration file requires no more than three lines of code. But what do you do when your contracts are tight together, you have libraries and the constructors require arguments?

This post will make everything clear. It will untangle every aspect of migrations:

  1. Basics of a migration
  2. Libraries (*and Interfaces)
  3. Arguments
  4. Interdependent Contracts

Basics of a migration 

Firstly, let’s look at what migrations mean. 

Generally, this term refers to transferring a system from one operating environment to another.

In Truffle, it means deploying your smart contracts to the blockchain of your choice. This is done with the help of the JavaScript files which you add to the migrations folder. They handle the deployment of your contracts: pushing them to the blockchain, passing parameters and linking contracts.

Before learning to migrate interdependent contracts with arguments & libraries, let’s understand the purpose of the initial migration.

Initial migration

There are two “helper files” that monitor the deployment of your contracts: Migrations.sol and 1_initial_migration.js. They appear in your project’s directory once you execute the truffle init command.

Initial Migration files: interdependent contracts and libraries

Their purpose is to track successful migrations. Essentially, saving gas by avoiding redeploying code.

////////// Migrations.sol ////////

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Migrations {
  address public owner = msg.sender;
  uint public last_completed_migration;

  modifier restricted() {
    require(
      msg.sender == owner,
      "This function is restricted to the contract's owner"
    );
    _;
  }

  function setCompleted(uint completed) public restricted {
    last_completed_migration = completed;
  }
}
////////// 1_initial_migration.js /////////

const Migrations = artifacts.require("Migrations");

module.exports = function (deployer) {
  deployer.deploy(Migrations);
};

1_initial_migration.js is a good example of how a migration file should look. It deploys the Migrations contract, which is built to remember the last completed migration. You trigger the setCompleted function whenever a new migration is executed. So this is how Truffle keeps track of the last successful migration.

Most likely you don’t need to modify these two files, so don’t worry about them.

We notice that Migrations.sol has to be deployed before any other contract, that’s why its prefix is 1. With the truffle migrate command, each JS migration file is executed in ascending order of its prefix.

When are migrations executed

As mentioned above, the initial migration files keep track of the last successful migration. When you add new JavaScript migration files and run the command truffle migrate, only the newly added ones will run. 

Unfortunately, even though you modified a contract that had been part of a successful migration, running truffle migrate will not redeploy it. In the terminal you might see the message “Network up to date”. 

To redeploy all your contracts use truffle migrate --reset

Also, the JavaScript migration code is executed every time you test your contracts (truffle test). Truffle wants you to have a clean environment before running your tests, therefore, it will redeploy everything.

Simple migration example

Let’s look at the code for a simple migration and understand each component.

The example contract we want to deploy is the following:

pragma solidity >=0.4.22 <0.9.0;

contract Vote {

  mapping (address => uint) public votes;
  address winner;
  uint public winner_votes;
  address owner;

  constructor(){
    owner = msg.sender;
  }

  function vote(address user) public {
    votes[user] += 1;
    if(votes[user] > winner_votes){
      winner_votes = votes[user];
      winner = user
    }
  }

}

Note that we have to name the migration with a higher prefix than 1 (2_vote_migration.js), in order for it to be deployed after the initial migrations. Create this new .js file inside the migrations folder.

const Vote = artifacts.require("Vote");

module.exports = function (deployer) {
  deployer.deploy(Vote);
};

The 1st line in 2_vote_migration.js (artifacts.require()) saves a JS contract abstraction in the variable named Vote, which we can use to reference the contract’s details. Next, we export a function that receives as parameters the Truffle deployer and uses it to deploy the contract.

For this tutorial, I chose to deploy on a Ganache local network. If you are not aware of what Ganache is or how to connect to it, I suggest reading this article: How to work with Truffle and Ganache.

Now, if you run truffle migrate, your simple contracts have been deployed to your local network. You can notice in your Ganache console that 3 transactions have been executed. One for the Migrations.sol, one for Vote.sol and one for…..?

The last one was a trigger to the Migrations.sol contract to remember the last migration.


Migrate libraries (+ interfaces)

On one hand, interfaces and abstract contracts cannot be deployed. You cannot push to the Ethereum network only function definitions, you need the logic. Although, using them can help you spot errors and organise your code. Also, there is no need to link interfaces in the migrations if your contract inherits it. 

On the other hand, if you are using libraries, you have to deploy and link them to your contracts.

So take the example above (Vote.sol) that uses SafeMath instead of addition. Because this is a library deployed by OpenZeppelin, referencing it can be done in two ways: 

  • Internal Code – copy paste the SafeMath.sol contract in your project
  • External Code – install the library

Treated as internal

If you are willing to redeploy this library (and consume gas) or you are working with a library you created, treat it as internal. This means that the contract for the library should be found in the contracts folder and, at compilation, it should have a build JSON file.

Now let’s say we are using the SafeMath library in our new Vote contract as highlighted:

pragma solidity >=0.4.22 <0.9.0;

import './SafeMath.sol'; // ADDED

contract Vote {

  mapping (address => uint) public votes;
  address winner;
  uint public winner_votes;
  address owner;

  constructor(){
    owner = msg.sender;
  }

  function vote(address user) public {
    votes[user] = SafeMath.add(votes[user], 1); // ADDED
    if(votes[user] > winner_votes){
      winner_votes = votes[user];
      winner = user;
    }
  }

}

In the migrations file, you should follow these steps: deploy library, link to the destination contract, deploy the contract.

const Vote = artifacts.require("Vote");
const SafeMath = artifacts.require("SafeMath");

module.exports = function (deployer) {
  deployer.deploy(SafeMath);
  deployer.link(SafeMath, Vote);
  deployer.deploy(Vote);
};

You can also link your library to multiple contracts at the same time using an array. Take the following example: 

deployer.link(SafeMath, [Contract1, Contract2, Contract3]);

Treated as external

If you don’t want to redeploy the library, then you should reference the source in your smart contracts.

Firstly, install the library using npm. In our case we want to install the OpenZeppelin contracts:

npm install @openzeppelin/contracts

Then, make sure you import the library:

pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/utils/math/SafeMath.sol"; // ADDED

contract Vote {

  mapping (address => uint) public votes;
  address winner;
  uint public winner_votes;
  address owner;

  constructor(){
    owner = msg.sender;
  }

  function vote(address user) public{
    votes[user] = SafeMath.add(votes[user], 1);
    if(votes[user] > winner_votes){
      winner_votes = votes[user];
      winner = user;
    }
  }

}

In the Truffle migration file, you only have to deploy your Vote.sol contract. No need to link the library or deploy it.

Arguments

When deploying a contract with Truffle, passing arguments to the constructor is very easy. Just add the values separated by “,”.

Take this example where we need 2 parameters, a string and an uint:

const Contract = artifacts.require("Contract");

module.exports = function (deployer) {
  deployer.deploy(Contract, "Josh",55);
};

Migrate interdependent Contracts

Truffle allows you to nicely handle multiple contracts and their deployment aftermath. Therefore, you can simply link your contracts together and allow them to interact after you migrate.

Let’s take a simple example where two contracts are interdependent. For Vote.sol to work it needs the functionality of Favourite.sol. On top of that, let’s make the Vote contract the only one that can call the methods of Favourite (we don’t want any other address to interact with this contract).

To do the above, we need both contracts to know the address of each other. The solution is to have Vote receive the address of Favourite through its constructor and Favourite have a setter. 

Vote.sol:

pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "./Favourite.sol";

contract Vote {

  mapping (address => uint) public votes;
  address winner;
  address owner;
  uint public winner_votes;
  Favourite favouriteContract;


  constructor(address _favouriteAddress){
    owner = msg.sender;
    // bonus poins for the favourite player
    favouriteContract = Favourite(_favouriteAddress);
  }

  function vote(address user) public{
    // favourite player has a head start
    if(favouriteContract.isFavourite(user) && votes[user] == 0){
      votes[user] = 1;
    }
    votes[user] = SafeMath.add(votes[user], 1);
    // user wins if it has the most votes
    if(votes[user] > winner_votes){
      winner_votes = votes[user];
      winner = user;
    }
  }


}

Favourite.sol:

pragma solidity >=0.4.22 <0.9.0;

contract Favourite {

  address private favouriteWinner;
  address voteAddress;
  address owner;

  modifier onlyVote() {
      require(msg.sender == voteAddress, "Only Vote.sol can use");
      _;
  }

  modifier onlyOwner() {
      require(msg.sender == owner, "Only owner can use");
      _;
  }

  constructor(address _favouriteWinner){
    favouriteWinner = _favouriteWinner;
    owner = msg.sender;
  }

  function isFavourite(address user) public view onlyVote returns(bool){
    return (favouriteWinner == user);
  }

  function setVoteAddress(address _voteAddress) public onlyOwner{
    voteAddress = _voteAddress;
  }

}

We cannot exchange addresses both ways at deployment (through the constructors), because it is only possible to deploy one contract at a time.

Truffle migrations allow us to do this address management very easily. Deploy the Favourite.sol contract. Then deploy Vote.sol and pass as argument the address of Favourite. Then we can set the Vote contract address in the Favourite contract.

const Vote = artifacts.require("Vote");
const Favourite = artifacts.require("Favourite");
const favouritePlayer = "0xa79b684e24Bd85fa5784c598c3690b68Dc93a058";

module.exports = function (deployer) {
  deployer.deploy(Favourite, favouritePlayer).then(async () => {
    // get instance of deployed Favourite contract
    const fInstance = await Favourite.deployed();
    // deploy and pass arguments
    await deployer.deploy(Vote, fInstance.address);
    // get instance of deployed Vote contract
    const vInstance = await Vote.deployed();
    // set Vote address in Favourite
    await fInstance.setVoteAddress(vInstance.address);
  });
};

Congratulations! With this final example you learned how to migrate interdependent contracts with libraries and arguments at the same time.


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

Leave a Reply