How to interact with another smart contract

How to interact with another smart contract

Hello readers👋

Smart contracts are the most popular feature of the Ethereum blockchain. Sometimes some smart contract functionality is needed to be accessed from outside the contract say by other contracts. Let us look at how to interact with a deployed smart contract and access its functions.

Let's get started.

Why do smart contracts need to interact with other contracts?

As normal computer programs or applications access other applications' data through an API, similarly smart contracts can access functions of other smart contracts that have been deployed on the blockchain. This reusability can save time and space on the blockchain since you don't need to write or copy and paste a function and you don't need to store a duplicate of the function on-chain.

Methods to interact with another smart contract from your smart contract

Let us look at some of the methods on how to interact with another smart contract from your contract.

First, let us understand the basic contract we are going to use.

Contract Counter -

contract Counter {
    uint public count;

    function increment() external {
        count = count + 1;
    }
}

This is a basic contract that contains a count variable and an increment function that increments the count by 1.

We will create another contract say 'Interaction' which will interact with this contract i.e say access the count variable or call the increment function.

1. Contracts exist in the same file

Suppose the contract 'Counter' and 'Interaction' both exist in the same file. Then the methods of the Counter can be easily accessed as follows (The complete code of the file is as follows) -

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Counter {
    uint public count;

    function increment() external {
        count = count + 1;
    }
}

contract Interaction {
    address counterAddress;

    function setCounterAddress(address _counterAddress) public {
        counterAddress = _counterAddress;
    }

    function getCount() external view returns(uint) {
        Counter instance = Counter(counterAddress);
        return instance.count();
    }
}

First, let us understand the contract 'Interaction'.

  • It contains an address variable representing the address where contract 'Counter' is deployed.
  • It contains a setCounterAddress() function which is used to set the contract address of the contract 'Counter' in the contract 'Interaction'.
  • It also contains a getCount() function which fetches the count variable from contract 'Counter'. This is how we interact with another smart contract.

To understand how this works, you first need to deploy the contract 'Counter' and copy its contract address. Then deploy the contract 'Interaction' and then call the setCounterAddress() function of this contract passing the copied contract address of the contract 'Counter'.

Now, call the increment() function in contract 'Counter' and getCount() function in contract 'Interaction' and see how count updates.

We see that here we use the following two lines of code to interact with another smart contract.

Counter instance = Counter(counterAddress);
return instance.count();

2. Contracts exist in different files

Suppose now contracts exist in different files - say the contract 'Counter' exists in the file 'Counter.sol' and the contract 'Interaction' exist in the file 'Interaction.sol'. The code of both files is as follows -

Counter.sol -

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Counter {
    uint public count;

    function increment() external {
        count = count + 1;
    }
}

Interaction.sol -

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./Counter.sol"; // Path where Counter.sol exists

contract Interaction {
    address counterAddress;

    function setCounterAddress(address _counterAddress) public {
        counterAddress = _counterAddress;
    }

    function getCount() external view returns(uint) {
        Counter instance = Counter(counterAddress);
        return instance.count();
    }
}

As you can see, here we use the import keyword to interact with the contract 'Counter'.

import "./Counter.sol";

Now to understand how this works, do the same as done above i.e deploy contract 'Counter' first and then deploy contract 'Interaction' and call methods respectively.

3. Interact with a standard smart contract

Suppose you want to interact with another contract not created by you i.e. some standard smart contract available to everyone.
Consider for example ERC-721 smart contract which is the standard smart contract for NFTs (non-fungible tokens).

You can look at the logic of the ERC721 smart contract here

Now we want to create a smart contract that interacts with this contract. Suppose we create a contract named 'OwnerInformation' which gives the owner address of any NFT that follows the ERC-721 standard.

'OwnerInformation.sol' -

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

// Import the ERC 721 smart contract
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract OwnerInformation {
    function getOwner(address nftCollectionAddress, uint tokenId) public view returns(address) {
        ERC721 nft = ERC721(nftCollectionAddress);
        return nft.ownerOf(tokenId);
    }
}

The above code gives the owner address of any NFT passing the NFT collection address and the token id inside the collection.

If you want to test this contract on your own, first make sure to deploy it on the Rinkeby network instead of your local blockchain, and then pass the respective parameters to the getOwner() function.

For example:
You can use the following parameters -
nftCollectionAddress - 0x85aD07229d36b48F291C82B57e8B481A323Ef11E
tokenId - 19

The above NFT details can be found here (LokiDevil NFT of MCU-NFT collection).
You can compare the owner address you get from your contract and the owner address of the above NFT on opensea.

You can use the above contract to determine the owner of any NFT that follows the ERC-721 standard]. You just need to know the nftCollectionAddress and the token id of that NFT inside the collection.

4. Use of interface

Suppose the contract 'Counter' exists in the file 'Counter.sol' and the contract 'Interaction' exists in the file 'Interaction.sol'. We can also interact with the contract 'Counter' without using the import statement. Instead, interact by creating an interface.

Interaction.sol -

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface MyCounter {
    function count() external view returns (uint);
    function increment() external;
}

contract Interaction {
    address counterAddress;

    function setCounterAddress(address _counterAddress) public {
        counterAddress = _counterAddress;
    }

    function getCount() external view returns(uint) {
        MyCounter instance = MyCounter(counterAddress);
        return instance.count();
    }
}

We created an interface named 'MyCounter' with the function signatures of the contract 'Counter', supplying two function signatures count and increment.

In the getCount() method, we created an instance of the 'MyCounter' interface and called the count() function on that which automatically calls count() on the contract 'Counter'.

Work with this code in the same way as done in the first two cases to understand how this method works to interact with another smart contract.

5. Use of call() / Interacting with contracts whose ABI is not known

Until now, we discussed methods to call smart contracts functions which used the smart contract ABI through the following statement -

Counter instance = Counter(counterAddress);

But what if we don't have the ABI of the contract which we want to interact with. In those cases, we use the call() functionality.

To understand this, let us first update the Counter.sol file as follows (Add the setCount() functionality).

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Counter {
    uint public count;

    function increment() external {
        count = count + 1;
    }

    function setCount(uint _count) external {
        count = _count;
    }
}

And the Interaction.sol file is updated as follows -

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Interaction {
    address counterAddress;

    function setCounterAddress(address _counterAddress) public {
        counterAddress = _counterAddress;
    }

    function getCount() external returns(uint) {
        (, bytes memory result) = counterAddress.call(abi.encodeWithSignature("count()"));
        uint val = abi.decode(result, (uint));
        return val;
    }

    function setCount(uint _val) public returns(bool) {
        (bool success,  ) = counterAddress.call(abi.encodeWithSignature("setCount(uint256)", _val));
        return success;
    }
}

Let us understand the above code now. First set the counterAddress variable using the setCounterAddress(address) functionality.

The setCount(val) function in 'Interaction.sol' interacts with the setCount(val) function of the contract 'Counter' by using the following statement -

(bool success,  ) = counterAddress.call(abi.encodeWithSignature("setCount(uint256)", _val));

The abi.encodeWithSignature() expects one or more parameters - one being the function name to be called and the rest being the arguments for the respective function.

The variable success stores whether the following transaction to call function of another smart contract succeeded or not.

The getCount() function in 'Interaction.sol' returns the current count value as it interacts with the count() of the contract 'Counter' by using the following statement -

(, bytes memory result) = counterAddress.call(abi.encodeWithSignature("count()"));

The explanation for this statement is the same as above. This also fetches the return value and stores it inside bytes memory which can be converted to uint or any other data type using the abi.decode method.

You can test both the setCount() and getCount() functions and observe how it works and understand how they interact with the 'Counter' smart contract.

Note -
If you want to send ether to a function using call() then the following syntax is used -

(bool success,  ) = counterAddress.call{value: 1 ether}(abi.encodeWithSignature("someFunctionName(uint256)", _val));

6. Use of delegatecall()

  • call - Execute the code of another contract
  • delegatecall - Execute the code of another contract, but with the state(storage) of the calling contract.

Remember delegate call means that you're calling a different contract code but the storage is set in the current contract.

Context when the contract calls another contract - image.png

Context when contract delegatecall another contract - image.png

Interact with smart contract in JS

You need to make sure you have web3.js installed. If not installed, you can install it using the following command -

npm install web3 --save

Import web3 using the following statement -

const Web3 = require("web3");

Set up your Infura endpoint

const web3 = new Web3('https://rinkeby.infura.io/v3/YOUR_PRIVATE_KEY');

To interact with a smart contract in the front end, we need its address and ABI. Let us say the contract address and contract abi are stored in the variables contractAddress and contractABI.

const contractAddress = /*Your contract address*/;
const contractABI = /*Your contract ABI*/;

Then you need to instantiate your smart contract as follows -

const contract = new web3.eth.Contract(contractABI, contractAddress);

To call a function of the smart contract in JS to read the state of the blockchain, we use the call() statement -

await contract.methods.methodName.call(function(err,res) {
      if (err) {
           console.log("Error");
           return;
      }
      console.log("Success");
});

To call a function of the smart contract in JS to update the state of the blockchain i.e send a transaction, we use the send() statement -

const senderAddress = /*Your address*/;
await contract.methods.methodName.send(
    {
         from: senderAddress
    }, 
    function(err,res) {
        if (err) {
             console.log("Error");
             return;
        }
        console.log("Success");
    }
);

Conclusion

  • This is the end of the blog. Now, you are able to interact with another smart contract.
    All the best!

  • I hope you find it helpful. Let me know your thought in the comment below.

  • Do like this blog and follow me!

  • Also, you could follow me on Twitter

Thanks for reading💛