Introduction
In my last article, we got started with Ethereum development by using Truffle and the Ganache command line. In this article, we will use this environment to explore two important concepts in Solidity: Interfaces and Function Modifiers.
Setting things up
Assuming you have Truffle and ganache-cli already installed, create a new folder and run truffle init
inside. Refer to the last article if you haven't done that already. We will create two contracts in the contracts folder truffle just created. Our example will be very simple.
- Answers.sol
pragma solidity ^0.4.18;
contract Answers {
uint answerUniverse;
function setAnswerUniverse(uint _answer) external {
answerUniverse = _answer;
}
function getAnswerUniverse() public view returns (uint){
return answerUniverse;
}
}
- Questions.sol
pragma solidity ^0.4.18;
contract AnswersInterface {
function getAnswerUniverse() public view returns (uint);
}
contract Questions {
AnswersInterface answersContract;
function setAnswersContractAddress(address _address) external{
answersContract = AnswersInterface(_address);
}
function whatIsTheAnswerUniverse() public view returns (uint){
uint answer = answersContract.getAnswerUniverse();
return answer;
}
}
The Answers.sol contract is very simple. There is a getter and a setter for one variable answerUniverse. Note, because it is important, that the setter is external, meaning it has to be called from outside the contract, and the getter is public, meaning everyone can call it.
The Questions.sol contract contains two things: the Questions contract, and the AnswersInterface interface. So, what is an interface? An interface allows you to talk to another contract on the blockchain. As you can see, to define an interface, you start out like a regular contract, with the contract keyword. Inside this contract, you only define the functions you want to interact with, without their bodies. Those functions need to be public or external so we can call them from outside the original contract. You couldn't interact with private or internal functions of course.
Inside our Questions contract, we create an interface instance in answersContract. The function setAnswersContractAddress will tell our contract where to look for the original Answers contract. The function whatIsTheAnswerUniverse retrieves the variable answerUniverse in the Answers contract.
Note: Of course, we will need to set the contract's address before retrieving the variable. If not, we wouldn't know where to look on the blockchain!
- Next, in the truffle-config.js file:
module.exports = {
networks: {
development: {
host: '127.0.0.1',
port: 7545,
network_id: '*'
}
}
};
For Windows users, you will have to remove the truffle.js file to avoid conflicts. For others, you can keep both and put this code in truffle.js, or do like a Windows user, doesn't matter.
Next, in the migrations folder, create two files: 2_deploy_questions.js and 3_deploy_answers.js.
2_deploy_questions.js
var Questions = artifacts.require("Questions")
module.exports = function(deployer) {
deployer.deploy(Questions)
};
- 3_deploy_answers.js
var Answers = artifacts.require("./Answers.sol")
module.exports = function(deployer) {
deployer.deploy(Answers)
}
Deplooooooyyy
Everything is ready now. Open a new terminal window and run ganache-cli -p 7545
.
Go back to the project's folder and run:
truffle compile
truffle migrate --network development
truffle console --network development
Now, we can play with our contracts and our interface. First, create an instance for each contract:
truffle(development)> Questions.deployed().then(inst => Questions = inst)
truffle(development)> Answers.deployed().then(inst => Answers = inst)
After launching the Answers instance, you will see some stuff appear in the console. There will be an address field. This is the address of the Answers' contract. This is what we needed to call the setAnswersContractAddress function:
truffle(development)> Questions.setAnswersContractAddress('0x2e91a07090cfbbc0839e0d76d8110e2518bae18c')
Note: Of course, replace the address with whatever address you will find in that field.
Now, let's set the answersUniverse variable in the Answers contract:
truffle(development)> Answers.setAnswerUniverse(76)
And retrieve it from the Questions contract:
truffle(development)> Questions.whatIsTheAnswerUniverse().then(answer => answer.toNumber())
76
Let's modify the answer, and retrieve it again:
truffle(development)> Answers.setAnswerUniverse(42)
truffle(development)> Questions.whatIsTheAnswerUniverse().then(answer => answer.toNumber())
42
Tadaaaaaa! That's an interface for you :)
Function modifiers
Now, you might have seen a security issue in our project. The function setAnswersContractAddress is external, meaning that everyone can call it from outside the contract. So, anybody could call the function and change the address!!! No good. To solve this, we will add a function modifier. A modifier is called before the function is executed. Basically, it runs some checks to make sure everything is fine. In our case, we will make sure that this function is called by the owner only. To do this, we will use a popular contract from the OpenZeppelin Solidity library called Ownable. I will copy and paste it.
Don't worry if you don't understand everything in this contract. Just know that it makes sure our function will be called by the owner only.
- Create a Ownable.sol file in contracts
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
}
- Import our Ownable contract in Questions and specify the function modifier for setAnswersContractAddress:
pragma solidity ^0.4.18;
import "./Ownable.sol";
contract AnswersInterface {
function getAnswerUniverse() public view returns (uint);
}
contract Questions is Ownable{
AnswersInterface answersContract;
function setAnswersContractAddress(address _address) external onlyOwner{
answersContract = AnswersInterface(_address);
}
function whatIsTheAnswerUniverse() public view returns (uint){
uint answer = answersContract.getAnswerUniverse();
return answer;
}
}
Relaunch the ganache-cli command, and the truffle commands. Deploy our Answers and Questions contracts. By default, the contract's owner is the first account created by ganache-cli. So, we'll take another account:
truffle(development)> account = web3.eth.accounts[3]
Now, if we try to set the interface address from this account, we will get an error:
truffle(development)> Questions.setAnswersContractAddress('0x37eba1bb7d4c779474a4955437e524fbdbac0dc2', {from: account})
Error: VM Exception while processing transaction: revert
If you remove the object argument, or use the accounts[0] address in the from key, you will be able to set the address just fine.
That's it for interfaces and function modifiers. Have fun!!!