Are you curious how multiple solidity contracts interact and how you can use this in your own smart contract setup? In this post we will use two contracts to create a trust fund application that pays you interest in a utility token, another contract.

We are learning by creating here. If you like to dive into the docs first take a look at the solidity language website here.

I will assume that you have some basic knowledge on solidity. If you do not know how to setup your development environment you can follow this previous post on the truffle suite. We also made the basic setup of our trust fund in this post. The contract for the utility token, that we will use for rewards, featured in this previous post on ERC20 tokens. Everything is in place to tight these multiple solidity contracts together.

The complete code for this post can be found on github here.

Recap

As a small recap of the previously build contracts we see here how we create a trust in our contract.

trusts[_beneficiary].push(FutureRelease(msg.value, block.timestamp, _releaseTime));

We have a mapping where we store FutureReleases for any address. The we store the amount, the starting time and the time the trust can be released.

Now we take a look at what is done when the trust fund is released (and thus has passed all the requirements).

// Release principal
msg.sender.transfer(futureRelease.amount);

// Emit event
emit TrustReleased(msg.sender, futureRelease.amount, 0, futureRelease.startTime, block.timestamp);

// Reset trust
delete trusts[msg.sender][i];

We see that the principal amount is transferred, this is made public by an event, and the released trust is deleted.

In addition to this principle amount we want to reward our users with interest in the form of the utility token TFT we created in this previous post on ERC20 tokens. So we have to be able to call the mint function on the token contract.

Get a solidity contract by address

To get access to another solidity contract we need to know where the code for this contract is located i.e. we need the address of the contract. In the output of truffle migrate we already could see the contract address.

truffle(develop)> migrate

Compiling your contracts...
===========================

...

2_deploy_trustfund.js
=====================

   Replacing 'TrustFundToken'
   --------------------------
   > transaction hash:    0xe3ba392f2731ba8a802d77caa42dffd664bcc7c152561013eb3392227a4a93c2
   > Blocks: 0            Seconds: 0
   > contract address:    0xd6A75da9f5b751262A6E95fBC080f41EBa62D36d
   > block number:        3
   > block timestamp:     1632122351
   > account:             0xA060590F69D9CAb96c8E07d12B07859AF46f8657
   > balance:             99.996774656
   > gas used:            1350277 (0x149a85)
   > gas price:           2 gwei
   > value sent:          0 ETH
   > total cost:          0.002700554 ETH
...

If we have this contract address string we can instantiate a contract object from this address. From this object we can call the methods defined in the contract.

truffle(develop)> let trustfundtoken = await TrustFundToken.at('0xd6A75da9f5b751262A6E95fBC080f41EBa62D36d')
undefined

truffle(develop)> trustfundtoken.mint(accounts[1], 2)
{
  tx: '0xaf02556104def95220447f9a587ba95d476c0e063de11ae9179c9d8c01c4a8cd',
  ...
}

Note that we need the information on the contract’s methods here, otherwise the mint function is not known. In the truffle console this is available by just using the name (TrustFundToken) but in general we need a representation of the contract interface. In solidity this is called the Application Binary Interface (ABI). All our ABI’s are found in the build folder.

Schermafbeelding 2021 09 20 om 10.02.31 - MULTIPLE SOLIDITY CONTRACTS

In truffle we usually deploy the contract we want access to. So there is a handy helper function to get access to the just deployed contract without having to copy the address.

truffle(develop)> trustfundtoken = await TrustFundToken.deployed()
undefined

truffle(develop)> tft.address
'0xd6A75da9f5b751262A6E95fBC080f41EBa62D36d'

Constructor

So we need the address of the token contract in the trust fund contract. Because these live in the same development environment we can do the following:

import "./TrustFundToken.sol";

contract TrustFund {

    TrustFundToken private token;

    constructor(TrustFundToken _token) {
        token = _token;
    }

    ...

}

We import the contract by referencing the contract definition file. Then we assume the token contract when the constructor is called (when this contract is deployed) and save to a private variable of the contract.

Now we can call the public functions on this locally saved token contract. The releaseFunds function now not only releases the principal amount, but also sends the caller the intrest in our utility trust fund token.

// Release principal
msg.sender.transfer(futureRelease.amount);

// Release TFT
... some calculation of the amount of intrest ...
token.mint(msg.sender, interest);

// Emit event
emit TrustReleased(msg.sender, futureRelease.amount, interest, futureRelease.startTime, block.timestamp);

// Reset trust
delete trusts[msg.sender][i];

Deploy the contracts

When we try to deploy and run our new changes we get the following error.

> truffle develop

truffle(develop)> migrate --reset
...

truffle(develop)> let instance = await TrustFund.deployed()
undefined

truffle(develop)> let accounts = await web3.eth.getAccounts()
undefined

truffle(develop)> tf.createTrust(accounts[1], 1631977078, { value: web3.utils.toWei('1', 'ether') })
{
  tx: '0x1f71638597a14803b683ac6051d6a9de4c0928e3ed1edcd441b3b6f9658b0e94',
...
}

< wait a minute until 1631977078 is passed >

truffle(develop)> tf.releaseFunds({from:accounts[1]})
Uncaught:
Error: Returned error: VM Exception while processing transaction: revert Error, sender is not a minter -- Reason given: Error, sender is not a minter.

Whow! This is the security feature we build into our token contract so only the minter can create new tokens. (To read more on why and how this is made take a look at this post on ERC20 tokens)

So we have to pass the minter role to the trust fund contract right after it is created. This is done in the migration scripts. (Here /migrations/2_deploy_trustfund.js)

module.exports = async function(deployer) {
  // Deploy token contract
  await deployer.deploy(TrustFundToken);
  const token = await TrustFundToken.deployed();

  // Deploy trustfund contract
  await deployer.deploy(TrustFund, token.address);
  const trustFund = await TrustFund.deployed();

  // Delegate minter role to trust fund contract
  await token.transferMinterRole(trustFund.address);
};

Note that we get both contracts after they are created with the deployed() function. Then we get the address of the trust fund contract and call the the transferMinterRole method on the token contract.

If we now replay the steps of the script above we see that this setup does the job.

truffle(develop)> tf.releaseFunds({from:accounts[1]})
{
  tx: '0x326c9fdc27d8d8996bf7370d1989c125b70ceb7d0c74fa968be669bac4fe3ccc',
  ...
}

truffle(develop)> web3.eth.getBalance(accounts[1])
'100999706774000000000'

truffle(develop)> token.balanceOf(accounts[1])
BN {
  negative: 0,
  words: [ 19056003, 708, <1 empty item> ],
  length: 2,
  red: null
}

Both the principal amount and the intrest are transferred to the beneficiary account.

Testing multiple contracts

With the change in the constructor all our tests for this contract fail. We need to take a look at the setup of these test.

describe("Create a trust", function() {
  it("should not create a trust in the past", function() {
    return TrustFund.new().then(async function(instance) {
...

Note that we call TrustFund.new() without any parameters. We need to provide the address of an instance of the deployed token contract. Let’s do this in one place that we can reuse in all tests.

beforeEach(async function () {
  tokenInstance = await TrustFundToken.new();
});

We need to import the token contract and now we deploy a new token contract before each test. We we can use this instance to get the address of the deployed contract and use it in the other contract. Take a look at how we test that the intrest is payed if the funds are released.

it("should pay interest in TFT", function() {
  return TrustFund.new(tokenInstance.address).then(async function(instance) {
    trustFundInstance = instance;
    tokenInstance.transferMinterRole(trustFundInstance.address);
    balanceTracker = await balance.tracker(beneficiary);
    const secondsOfTrustRewards = 2;

    await trustFundInstance.createTrust(
        beneficiary,
        (await time.latest()).add(time.duration.seconds(secondsOfTrustRewards)),
        { value: weiToSend }
    );
    await time.increase(time.duration.seconds(secondsOfTrustRewards));

    await trustFundInstance.releaseFunds({from: beneficiary});

    return tokenInstance.balanceOf(beneficiary);
  }).then(async function(tokenBalance) {
    const apy = 10;
    const intrestPerSecond = weiToSend*apy/(100*365.25*24*60*60)
    expect(tokenBalance).to.be.bignumber.equals(new BN(intrestPerSecond*secondsOfTrustRewards));
  });
});

Note that we manually have to transfer the minter role to make this work. This what we want in a unit test to have low level control. In an end-to-end test we would like to work with the contracts as deployed with the migration scripts. This is possible with the deployed() method we already saw in the truffle console.

describe("Happy flow", function() {
  it("should pay interest", function() {
      return TrustFund.deployed().then(async function(instance) {

Note that the address of the token contract is also omitted.

Hopefully you are now able to make and test a system of multiple solidity contracts that interact with each other to have a more modular setup. Happy contracting!