This blog post is intended for developers to learn how to write and deploy a Smart Contract on Ethereum using the Solidity programming language. The blog post will explain the basic development setup environment. Next, we will write a basic smart contract and a test script to ensure our Smart contract works as intended. We will also introduce advanced Solidity concepts such as inheritance and modifiers. Finally, we will learn how to compile and deploy our smart contract to a testnet of our choice.
The complete source code is available on my Github page.
https://github.com/watat83/First-Smart-Contract-Solidity
Setup Development Environment
Before we get started, make sure you have all the dependencies and necessary software and development environment needed to follow along. You can check out my blog post on A Comprehensive List of Ethereum Development Tools, Libraries, Frameworks that explains how to install Node.js, Truffle, OpenZeppelin, Ganache, MetaMask, and others.
Node.js Installation
To install Node.js, go to https://nodejs.org/en/, download the corresponding version for your operating system and install it. To verify that node.js was successfully installed, run the command
// Your version might be different than mine. And that's fine as long as you are above version 8.0.0
$ node -v
v16.1.0
$ npm -v
7.11.2
First order of business, let’s create a project directory called MyContract and CD into that directory. Our entire project will live inside it.
$ mkdir MyContract
$ cd MyContract
Next, inside the MyContract directory, we need to initialize a package.json file that will track all our dependencies. Run the following code
MyContract $ npm init -y
A package.json file will be created, along with a node_module directory where our dependencies will live.
Next, let’s create a .gitignore file and add files and directories we do not want our project to track.
// Create the .gitignore file
MyContract $ touch .gitignore
Inside that file, add the following code
node_modules/
.env
package-lock.json
.gitignore
OpenZeppelin Smart Contract Installation
To install the Smart Contract library from OpenZeppelin, run the following command
MyContract $ npm install @openzeppelin/contracts
Installing [============] 100%|
+ @openzeppelin/contracts
Ganache IDE Installation
To install the Ganache IDE, go to https://trufflesuite.com/ganache, download the version of Ganache for your operating system, and install it on your computer. Once the installation is complete, launch the application. You should see a screen that looks like this.
For this tutorial, click on the “Quickstart” button to quickly spin up a local blockchain ready for development. You should see the following screen, indicating that you have successfully installed and launched Ganache on your local machine.
Truffle Installation and Initialization
To install Truffle, run the following command
MyContract $ npm install -g truffle
+ truffle@5.4.17
installed 1 package in 37.508s
To make sure Truffle was installed successfully, run the following command
MyContract $ truffle version
//The output should look like this
Truffle v5.4.17 (core: 5.4.17)
Solidity v0.5.16 (solc-js)
Node v16.1.0
Web3.js v1.5.3
To get started with Truffle, we will initialize a new project.
//InsideMyContract, initialize truffleMyContract $ truffle init
When you run the initialization command, you should see an output that looks like this:
Starting init...
================
> Copying project files to <PATH_TO_MY_PROJECT_DIRECTORY>/MyContract
Init successful, sweet!
Try our scaffold commands to get started:
$ truffle create contract YourContractName # scaffold a contract
$ truffle create test YourTestName # scaffold a test
http://trufflesuite.com/docs
The MyContract directory will look like this:
- The contracts directory is where we will write all our Smart Contracts code.
- The migrations directory is where we will write all our deployment scripts, following a specific convention.
- The test directory is where we will write all our Smart Contract tests.
- The truffle-config.js file is where we will set up network configuration, builds and artifacts directory, and more.
Now that we have initialized a new truffle project, we are ready to write our first smart contract.
Write Your First Smart Contract
Inside our contracts sub-directory, let’s create a file called myContract.sol with the following command:
MyContract $ touch contracts/myContract.sol
Let’s open the file and paste the following code:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define your Smart Contract with the "Contract" keyword
contract myContract {
}
As seen above, the first line defined the version of Solidity used to code the Smart Contract. The second line defines the smart contract itself, using the keyword contract. Contracts in Solidity behave as Classes in other object-oriented programming languages.
That’s it. Congratulations. You just wrote your first Smart Contract with Solidity on Ethereum. Next, we will attempt to write a test script for our smart contract.
How To Write Our First Smart Contract Test
In this section, we will write a test script in JavaScript for our smart contract to make sure it deploys correctly, and we will be using a TDD (Test Driven Development) approach throughout this tutorial.
Inside our test sub-directory, let’s create a file called myContract.test.js with the following command:
MyContract $ touch test/myContract.test.js
Next, we will add the following code inside our test file:
const MyContract = artifacts.require("MyContract"); // 1️⃣
contract("MyContract", () => { // 2️⃣
it("Contract Deployed Successfully!", async () => { // 3️⃣
const myContract = await MyContract.deployed();
assert(myContract, "Contract Deployment Failed!"); // 4️⃣
});
});
1️⃣ We load and interact with compiled contracts through artifacts.require() function. The name of the contract, not the name of the file is passed as an argument to the function.
2️⃣ Truffle uses the Mocha framework, but with the benefits of Truffle’s clean room, which means that contracts are deployed before tests are executed.
3️⃣ Because of the asynchronous nature of the Blockchain, we leverage the async/await syntax of JavaScript.
4️⃣ If the myContract variable exists, the test should pass.
To run our test, type the following command:
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--11853-whPVZvlrC0Nn
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
1) Contract Deployed Successfully!
> No events were emitted
0 passing (99ms)
1 failing
1) Contract: MyContract
Contract Deployed Successfully!:
Error: myContract has not been deployed to detected network (network/artifact mismatch)
at Object.checkNetworkArtifactMatch (/usr/local/lib/node_modules/truffle/build/webpack:/packages/contract/lib/utils/index.js:247:1)
at Function.deployed (/usr/local/lib/node_modules/truffle/build/webpack:/packages/contract/lib/contract/constructorMethods.js:83:1)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at Context.<anonymous> (test/myContract.test.js:5:28)
This error indicates that our contract was not successfully deployed on any network. This is because the truffle test command executes truffle compile and then truffle migrate in one shot. To fix this issue, we need to create a migration script for our Smart Contract. That will be the focus of the next section of this tutorial.
Smart Contract Compilation
In this section, we will write our first migration script in JavaScript, that will ensure that our smart contract gets deployed properly.
First, let’s create a migration file called 2_deploy_myContract.js
MyContract $ touch migrations/2_deploy_myContract.js
Next, we will add the following code to our migration script
const MyContract = artifacts.require("MyContract");
module.exports = function(deployer){
deployer.deploy(MyContract);
}
Now let’s run our test again
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--11894-7Pqa3CknRN8n
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
1 passing (116ms)
The test has finally passed, and this means that we are ready to add more advanced features to our Smart Contract and write more test scripts.
Solidity Advanced Features
In this section, we are going to expand on our smart contract and add variables and functions. We will also write test scripts against those variables and functions.
Solidity Functions
In Solidity, functions are defined within a Smart Contract and can be called or executed by an EOA or another Smart Contract. There are predefined functions built-in into the Solidity Language, and developers can write their custom functions.
Every Solidity function follows this syntax:
function functionName ([parameters]){public|private|internal|external}
[pure|constant|view|payable] [modifiers] [returns (return types)]
For our example, we will have a function called getString() that will return a string.
First, we will write our test script inside test/myContract.test.js
const MyContract = artifacts.require("MyContract");
contract ("MyContract", () => {
...
omitted code
...
describe("getString()", () => {
it("returns 'String'", async() => {
const myContract = await MyContract.deployed();
const expected = "String";
const actual = await myContract.getString();
assert.equal(actual, expected, "getString returned 'String' and did not update");
})
})
})
If we run the test once again, it will fail with the following error:
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--11909-Rn7Poy7bAFVd
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
1) returns 'String'
> No events were emitted
1 passing (106ms)
1 failing
1) Contract: MyContract
getString()
returns 'String':
TypeError: myContract.getString is not a function
at Context.<anonymous> (test/myContract.test.js:13:45)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
This error occurs because we have not declared a getString() function inside of our Smart Contract. Let’s do that right now.
Inside our contract, let’s add the following function:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define our Smart Contract
contract myContract {
function getString() external view returns(string memory) {
return "String";
}
}
If we now run our test, it should pass, and you should see something like this:
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--11927-SsbSzObZQ1eN
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (118ms)
2 passing (201ms)
Now that we can read from our smart contract, let’s add the ability to modify the value ‘String’ inside the contract.
Dynamic Functions in Solidity
Now we will add another function that will allow us to set a string value and return it. Our function will be called setString().
First, let’s write our test script inside myContract.test.js
const MyContract = artifacts.require("MyContract");
...
omitted code
...
contract ("MyContract: Set String", () => {
describe("setString(string)", () => {
it("sets the new string", async() => {
const myContract = await MyContract.deployed();
const expected = "New String";
await myContract.setString(expected);
const actual = await myContract.getString();
assert.equal(actual, expected, "setString did not update to 'New String'");
})
})
})
If we try to run the test once again, it will fail because of the same reason explained above. The contract could not find the setString() function.
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--11947-75FvHhB8Intu
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (60ms)
Contract: MyContract: Set String
setString(string)
1) sets the new string
> No events were emitted
2 passing (256ms)
1 failing
1) Contract: MyContract: Set String
setString(string)
sets the new string:
TypeError: myContract.setString is not a function
at Context.<anonymous> (test/myContract.test.js:27:30)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
Inside our smart contract, let’s add the setString() function
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define our Smart Contract
contract myContract {
function getString() external view returns(string memory) {
return "String";
} function setString(string calldata newString) external {
}
}
The function setString() is set up in a way that it can only be called by EOAs or other smart contracts. And because the parameter passed to the function is provided by an external entity and not part of the persistent storage of the smart contract, it has to have the keyword calldata appended to it.
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--11967-RYNpBSpYG0bb
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (58ms)
Contract: MyContract: Set String
setString(string)
1) sets the new string
> No events were emitted
2 passing (473ms)
1 failing
1) Contract: MyContract: Set String
setString(string)
sets the new string:
setString did not update to 'New String'
+ expected - actual
-String
+New String
at Context.<anonymous> (test/myContract.test.js:29:20)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
The test is failing is because to update a variable inside of a function, we need to declare it as a state variable which will be stored inside the smart contract persistent storage. That is what we will explore next.
State Variables in Solidity
We will do three things: add a state variable, update the setString() and getString() functions. Our smart contract will now look like this:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define our Smart Contract
contract myContract {
string private _myString;
function getString() external view returns(string memory) {
return_myString;
}
function setString(string calldatanewString) external {
_myString =newString
}
}
If we now run our test, it should not pass because we need to instantiate our state variable.
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--12091-cI2hFojXJSzr
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
1) returns 'String'
> No events were emitted
Contract: MyContract: Set String
setString(string)
✓ sets the new string (332ms)
2 passing (575ms)
1 failing
1) Contract: MyContract
getString()
returns 'String':
getString returned 'String'
+ expected - actual
+String
at Context.<anonymous> (test/myContract.test.js:15:20)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
Let’s initialize our state variable like so:
// ./contracts/MyContract.sol
[...omitted code...]string private _myString = "String";
[...omitted code...]
Now our test will all pass
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--12186-jKQ6iJ9DYx8g
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (85ms)
Contract: MyContract: Set String
setString(string)
✓ sets the new string (610ms)
3 passing (822ms)
Smart Contract Access Control
At the moment, anyone who has access to this smart contract can execute it and update the value of _myString, which is a security issue. To restrict access to this smart contract, we will make sure that only the address which deployed the smart contract can update the state variables of the contract.
Let’s start by writing a test script inside test/myContract.test.js that will ensure that an owner exists and that its address matches the one that deployed the smart contract.
// test/myContract.test.js
...
omitted code
...
contract("MyContract: Owner", (accounts) => {
describe("owner()", () => {
it("returns the address of the owner", async () => {
const myContract = await MyContract.deployed();
const owner = await myContract.owner();
assert(owner, "The Current Owner");
})
it("matches the address that originally deployed the contract", async () => {
const myContract = await MyContract.deployed();
const owner = await myContract.owner();
const expected = accounts[0];
assert.equal(owner, expected, "matches the address that deployed the contract");
})
})
})
Because we are in a test environment (ganache), we have access to the accounts variable. Our test will still fail because when we deploy the contract, we do not specify that we want the _owner to own the contract.
To do so, we will need to explicitly declare a constructor function and initialize the _owner variable. The constructor is the first function that is run when the smart contract is first deployed. So by setting the _owner variable inside the constructor, we will ensure that they are the owner of the contract. For this, we will use msg.sender.
Now let’s modify our smart contract, by adding the new variable owner, setting the owner, and adding a function
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define our Smart Contract
contract myContract {
string private _myString = "String";
address private _owner;
constructor() {
_owner = msg.sender;
}
function getString() external view returns(string memory) {
return_myString;
} function setString(string calldatanewString) external {
_myString =newString
} function owner() public view returns(address) {
return _owner;
}
}
If we run our tests once again, they will all pass.
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--12743-GIUy70nIUSG9
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (59ms)
Contract: MyContract: Owner
owner()
✓ returns the address of the owner (77ms)
✓ matches the address that originally deployed the contract (60ms)
Contract: MyContract: Set String
setString(string)
✓ sets the new string (843ms)
5 passing (1s)
This method works, but there is even a better way of doing this with modifiers.
Function Modifiers in Solidity
Modifier functions allow us to extend the capability of a contract function and execute it before/after the contract function itself.
Let’s edit test/myContract.test.js and add the following test:
// test/myContract.test.js
...
omitted code
...
contract("MyContract: Set String", (accounts) => {
describe("message was sent by another account", () => {
it("does not set the new string", async () => {
const myContract = await MyContract.deployed();
const expected = await myContract.getString();
try{
await myContract.setString("Not the owner", {from: accounts[1]});
}catch(err){
const errorMessage = "Ownable: caller is not the owner"; assert.equal(err.reason, errorMessage, "string should not update");
return;
}
assert(false, "string should not update");
})
})
})
Our test will fail because the address that deployed the contract (account[0]) and the address running the test on the contract (account[1]) don’t match.
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--12766-eekU3V76PpH7
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (68ms)
Contract: MyContract: Set String
setString(string)
✓ sets the new string (672ms)
Contract: MyContract: Owner
owner()
✓ returns the address of the owner (47ms)
✓ matches the address that originally deployed the contract (48ms)
Contract: MyContract: Set String
message was sent by another account
1) does not set the new string
> No events were emitted
5 passing (1s)
1 failing
1) Contract: MyContract: Set String
message was sent by another account
does not set the new string:
AssertionError: string should not update: expected undefined to equal 'Ownable: caller is not the owner'
at Context.<anonymous> (test/myContract.test.js:67:24)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
To fix this, we need to do two things: first, create the modifier function inside our smart contract, and then apply that modifier to the setString().
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define our Smart Contract
contract myContract {
string private _myString = "String";
address private _owner;
constructor() {
_owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == _owner, "Owner: caller is not the owner");
_;
}
function getString() external pure returns(string memory) {
return_myString;
} function setString(string calldatanewString) external onlyOwner {
_myString =newString
} function owner() public view returns(address) {
return _owner;
}
}
If we now run the truffle test command, we will see that they all pass as expected.
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--13424-ngHl0ifN3DVj
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (69ms)
Contract: MyContract: Owner
owner()
✓ returns the address of the owner (51ms)
✓ matches the address that originally deployed the contract (80ms)
Contract: MyContract: Set String
setString(string)
✓ sets the new string (636ms)
message was sent by another account
✓ does not set the new string (1501ms)
6 passing (3s)
It is all great. But there is a better way to manage access control, which entails using a library called OpenZeppelin Smart Contracts combined with a concept called Inheritance which we will discuss next.
Inheritance in Solidity: OpenZeppelin Ownable Contract
OpenZeppelin has a repository of smart contracts that have been developed and tested by experts in the industry. The cool thing about this is that we do not have to understand all the details behind the library but can import and use it inside our project. They published one smart contract called ownable.sol that handles ownership/access to smart contracts, which we will use in our example.
OpenZeppelin Installation
MyContract $ npm install --save @openzeppelin/contracts
Once installed, we will update our contract by importing the OpenZeppelin Ownable smart contract and applying it to the contract utilizing inheritance, as seen below:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Import OpenZeppelin Ownable Smart Contract
import "@openzeppelin/contracts/access/Ownable.sol";
// Define our Smart Contract
contract myContract is Ownable{
[... Omitted code...]
}
With this new setup, we do not need the onlyOwner modifier function, the _owner variable, and the constructor function, anymore because the Ownable contract takes care of ownership/access.
Our final Smart Contract ./contracts/myContract.sol will look like this:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Import OpenZeppelin Ownable Smart Contract
import "@openzeppelin/contracts/access/Ownable.sol";
// Define your Smart Contract with the "Contract" keyword
contract myContract is Ownable {
string private _myString = "String";
function getString() external view returns (string memory) {
return _myString;
}
function setString(string calldata newString) external {
_myString = newString;
}
}
Our final Smart Contract test script ./test/myContract.test.js will look like this
const MyContract = artifacts.require("MyContract"); // 1️⃣
contract("MyContract", () => { // 2️⃣
it("Contract Deployed Successfully!", async () => { // 3️⃣
const myContract = await MyContract.deployed();
assert(myContract, "Contract Deployment Failed!"); // 4️⃣
});
describe("getString()", () => {
it("returns 'String'", async () => {
const myContract = await MyContract.deployed();
const expected = "String";
const actual = await myContract.getString();
assert.equal(actual, expected, "getString returned 'String' and did not update");
})
})
});
contract("MyContract: Owner", (accounts) => {
describe("owner()", () => {
it("returns the address of the owner", async () => {
const myContract = await MyContract.deployed();
const owner = await myContract.owner();
assert(owner, "The Current Owner");
})
it("matches the address that originally deployed the contract", async () => {
const myContract = await MyContract.deployed();
const owner = await myContract.owner();
const expected = accounts[0];
assert.equal(owner, expected, "matches the address that deployed the contract");
})
})
})
contract("MyContract: Set String", (accounts) => {
describe("setString(string)", () => {
it("sets the new string", async () => {
const myContract = await MyContract.deployed();
const expected = "New String";
await myContract.setString(expected);
const actual = await myContract.getString();
assert.equal(actual, expected, "setString did not update to 'New String'");
})
})
describe("message sent by another account", () => {
it("does not set the new string", async () => {
const myContract = await MyContract.deployed();
const expected = await myContract.getString();
try {
await myContract.setString("Not the owner", {
from: accounts[1]
});
} catch (err) {
const errorMessage = "Ownable: caller is not the owner";
await assert.equal(err.reason, errorMessage, "The string should not update");
return;
}
assert(false, "string should not update");
})
})
})
Our migration script ./migrations/2_deploy_myContract.js has not changed throughout the tutorial.
const MyContract = artifacts.require("MyContract");
module.exports = function (deployer) {
deployer.deploy(MyContract);
}
Running our tests from the MyContract directory will output something like this:
MyContract $ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Compiling @openzeppelin/contracts/access/Ownable.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--14009-HF40FJ8EjMNv
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: MyContract
✓ Contract Deployed Successfully!
getString()
✓ returns 'String' (62ms)
Contract: MyContract: Owner
owner()
✓ returns the address of the owner (43ms)
✓ matches the address that originally deployed the contract (49ms)
Contract: MyContract: Set String
setString(string)
✓ sets the new string (571ms)
message sent by another account
✓ does not set the new string (1313ms)
6 passing (2s)
In this section, we learned some advanced features of Solidity, such as state variables, functions, special functions called modifiers. We also briefly touched on the concept of inheritance and demonstrated how to use it with OpenZeppelin’s Ownable contract inside our Smart Contract.
Now that we have explored these advanced concepts of Smart Contract Development using Solidity, it is time to step back and dive a little deeper into how Smart Contract Deployment works and learn how to deploy or Smart Contract onto our local blockchain (ganache) or on a public test net.
Smart Contract Deployment
Now that we have learned how to write a smart contract using solidity, it is time to shift our focus on deployment. In this section, we will explore how to deploy our solidity smart contract on a local test net, such as ganache. The advantage of using a local blockchain is that we can quickly experiment and test our application without paying network fees.
The next step will be to deploy our smart contract on the Rinkeby public test network using an Infura node. Infura provides a nice user interface that allows developers to quickly spin up an Ethereum node and manage keys, etc.
Let’s get it!
In our previous examples, we use the truffle test command which, runs the truffle compile and the truffle migrate commands consecutively. In the background, Truffle launches a local/virtual blockchain to compile, deploy, and then test our smart contract. At the end of the process, Truffle destroys the virtual network and its associated data. Nothing persists once the test is complete.
To deploy to a local or public network that will persist our contract and its data, we need to explicitly use the truffle migrate command, and specify the network we would like to deploy to, with the –network flag. Truffle is smart enough to look up those configurations inside the truffle-config.js file, located in the root of your project folder.
Before we go any further, let’s understand the smart contract deployment process.
Smart Contract Deployment Process
Every time we run truffle test or truffle migrate, Truffle is smart enough to run truffle compile in the background automatically. But if we need to compile our smart contract directly, we could run
MyContract $ truffle compile
// The output should similar to thisCompiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Compiling @openzeppelin/contracts/access/Ownable.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Artifacts written to ./build/contracts
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
This command will also generate a new directory ./build/contracts that will store the artifacts of our smart contracts in separate .json files.
Taking a look at build/contracts/myContract.json
We should focus on three specific fields inside that file: the Application Binary Interface or ABI. The ABI describes the functions and events of our smart contracts. This field is used by frontend applications to interact with the smart contract and call its functions.
The second field worth mentioning is the bytecode: a compiled version of our smart contract that the Ethereum network can understand, interpret and execute every time a client invokes the contract, state variables, or functions of the contract.
The last field to look at is the network. As we can see, this field is currently empty. This simply means that when we compiled the contract, there was no network specified inside the truffle-config.js file. Once we specify the network we want to use, this field will be populated appropriately. We will come back to it later in this tutorial.
When we deploy a smart contract, it is registered as a transaction on the Ethereum network, which, to be valid, will need a couple of things: a receiving address to the 0x0 address, and the bytecode inside build/contract/myContract.json, which will be sent as transaction data.
Before we could interact with our smart contract, it needs to be mined first. The mining mechanism will execute the code in the constructor of the smart contract which will set its initial state.
Truffle Configuration
Now that we understand how the deployment process of a smart contract works, we are now going to learn how to configure Truffle to specify where to deploy our artifacts/builds, and what test network to deploy our smart contract to.
If we want to change the directory where our artifacts and builds are generated, we need to modify our truffle-config.js file, and add the following line of code:
module.exports = {
contracts_build_directory: "./client/src/contracts",
// [...omitted code...]
}
If we recompile our project, we will have a new directory ./client/src/contracts, and inside we will have our artifacts files.
MyContract $ truffle compile
// command output
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/myContract.sol
> Compiling @openzeppelin/contracts/access/Ownable.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Artifacts written to ./client/src/contracts
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Now our artifact files are available to our client-side application. If need be, we can get rid of the ./build directory, given that our artifacts and builds will be generated on the client side. But it is up to the developer to determine how they want to structure their project and directories.
How To Deploy A Smart Contract To Ganache
To configure our project to work with our local ganache, let’s dive deep into the truffle-config.js file.
// const HDWalletProvider = require('@truffle/hdwallet-provider');
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
module.exports = {
contracts_build_directory: "./client/src/contracts",
// $ truffle test --network <network-name>
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websocket: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
// ropsten: {
// provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
// network_id: 3, // Ropsten's id
// gas: 5500000, // Ropsten has a lower block limit than mainnet
// confirmations: 2, // # of confs to wait between deployments. (default: 0)
// timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
// skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
// },
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
useColors: true,
// reporter: "json"
},
// Configure your compilers
compilers: {
solc: {
version: "0.8.9", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
// enabled: false,
// runs: 200
// },
// evmVersion: "byzantium"
// }
}
},
//
// After you backed up your artifacts you can utilize db by running migrate as follows:
// $ truffle migrate --reset --compile-all
//
// db: {
// enabled: false,
// host: "127.0.0.1",
// adapter: {
// name: "sqlite",
// settings: {
// directory: ".db"
// }
// }
// }
};
Assuming that we already have Ganache installed and up and running in the background, let’s open the dashboard. The main screen shows a list of all the accounts associated with this specific instance of Ganache. The dashboard also provides us with server information, which we need to configure the desired network we would like our smart contract to interact with (Ganache in this case).
Your Ganache dashboard should be like this:
As seen above, we have network information at the top of the screen.
- The current block: 0
- Gas Price: 20000000000 (20 gwei)
- Gas Limit: 6721975
- Network id: 5777
- RPC Server / host: 127.0.0.1:7545
- Port: 7545
We can use this information in our truffle-config.js file to connect to this specific ganache local instance.
First, we will add a new entry inside the network field of ./truffle-config.js.
[...omitted code...]
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
ganache: {
host: "127.0.0.1", // Ganache RPC Server (default: none)
port: 7545, // Current Ganache Instance port (default: none)
network_id: 5777, // Current Ganache Instance network ID (default: none)
},
[...omitted code...]
Once our configuration is complete, we will be able to access other variables, the full list of 10 fake ETH accounts, along with their private keys. It is important to note that these variables are used for development and testing purposes only. These fake Ethers cannot be used on the Ethereum main network to pay for transactions.
With our Truffle project fully configured and ready to connect to our local Ganache, we will now import our fake accounts into MetaMask, using the mnemonic provided by Ganache. If you are logged in to MetaMask with another account, make sure you log out and then click on “Import using account seed phrase” to gain access to Ganache-generated accounts.
Once your accounts are imported into MetaMask, they should be listed on your account tab like so:
With our Truffle project configured to connect to our local Ganache blockchain and our test accounts imported into MetaMask, we are now going to deploy. Deployments are considered network transactions and will incur a network fee.
To deploy our Smart Contract to the ganache network we just set up, run the following command inside your Truffle project root directory:
MyContract $ truffle migrate --network ganache
// We should see the following output
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_initial_migration.js
======================
Deploying 'Migrations'
----------------------
> transaction hash: 0xb9a9cbe3c3a0f9dd6cfd54fbde6f986d81b18964b8a29e24718d804f99c19960
> Blocks: 0 Seconds: 0
> contract address: 0x4E347c6b16D77B093C20a413110c220A40691FC1
> block number: 1
> block timestamp: 1636104397
> account: 0xC9d89462951Ca6A8E4579750109a9900268Ae6dE
> balance: 99.99502316
> gas used: 248842 (0x3cc0a)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00497684 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00497684 ETH
2_deploy_myContract.js
======================
Deploying 'myContract'
----------------------
> transaction hash: 0x95fd8494c031f673cdf2c5e4ac20a6a942311fc8db478a07b31b81cc930944c2
> Blocks: 0 Seconds: 0
> contract address: 0xBf49C4e184ECFA2E071BfB4b4f9384073B4fE3Ce
> block number: 3
> block timestamp: 1636104398
> account: 0xC9d89462951Ca6A8E4579750109a9900268Ae6dE
> balance: 99.98231866
> gas used: 592712 (0x90b48)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.01185424 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.01185424 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.01683108 ETH
Looking at the output above, we can confirm that our smart contracts have been deployed successfully to the Ganache local chain. Each smart contract that we deployed has a transaction receipt which has useful information, such as the transaction hash, the contract address (which is generated by Ethereum on deployment), the block timestamp (of when the deployment occurred), the account that deployed the contract, the gas used and the gas price, along with the value of ETH that was spent during the deployment.
From the output on the console, We can see that account[0] with address 0xC9d89462951Ca6A8E4579750109a9900268Ae6dE deployed both smart contracts and paid a total of 0.01683108 ETH in transactions.
Let’s go back to the Ganache dashboard. We see that account[0] with address 0xC9d89462951Ca6A8E4579750109a9900268Ae6dE now has a balance lower than the initial 100 ETH, and it has also registered four transactions in total.
If we click on the transactions tab inside Ganache, we can see all four transactions made by account[0]
If you correctly imported your Ganache-generated accounts into MetaMask, you should also see that account[0] balance was updated as well.
Congratulations! You just deployed your first Solidity smart contract to the Ganache local network. The next line of business is to deploy our contracts to a public test network, like Rinkeby, using an Infura connection to an Ethereum node.
How To Deploy A Smart Contract To Rinkeby With Infura
So far, we have been able to write and deploy our smart contract on the Ganache local network. It is great, but the problem is that we are the only ones who can interact with the Smart Contract. It is impossible to test it in a real-world environment like a public test network.
Using the Rinkeby network, along with Infura we avoid downloading a local blockchain or an Ethereum client. Infura will allow us to connect to an Ethereum node that it manages. Infura facilitates deployments to the mainnet, Rinkeby, Kovan, Goerli, and Ropsten networks.
To get started, go to https://infura.io/register and sign up for a free account. Once your create an account has been verified, you will get forwarded to the dashboard with a blank screen. Click on Create New Project button. Give your project a name like MyContract. Once created, you will get forwarded to the project page that provides critical variables such as project id, project secret, Infura endpoints that we will use to connect to our node instance on Infura.
It is important to note that because we chose to connect to the Rinkeby network, we should change the Endpoints field from MAINNET to RINKEBY. This is also going to update the Rinkeby endpoint we will use.
The first thing we need to do is store our Infura project ID into environment variables. To do so, we will do a couple of things:
- Install the dotenv package inside your project
- Create a .env file that will store our environment variables
// Install dotenv package
MyContract $ npm i --save dotenv
// Create a dotenv file to store environment variables
MyContract $ touch .env
Inside the .env file, add the following environment variables:
MNEMONIC="<YOUR_METAMASK_MNEMONIC>"
INFURA_PROJECT_ID="<YOUR_INFURA_PROJECT_ID>"
To get the mnemonic for your MetaMask accounts,
- Make sure you are on the Rinkeby test network
- Click on the account Icon
- Click Settings and select Security & Privacy
- On the Security & Privacy screen, click on Reveal Secret Recovery Phrase
- Enter your password and the next screen will reveal your seed phrase
- Copy your see phrase and update your .env file with the new value
The next step is to update our truffle-config.js file to include the Rinkeby configuration. But first, let’s import the dotenv package so we can access our environment variables
require('dotenv').config();
module.exports = {
[...omitted code...]
}
Next, we will install a package called @truffle/hdwallet-provider which is a Web3 provider that we will use to sign transactions for addresses derived from a 12 or 24-word mnemonic. To install it, run the command
MyContract $ npm i --save-dev @truffle/hdwallet-provider
Once installed, we will require it inside our project, and the code will look like this:
// Require the dotenv package inside our project
require('dotenv').config();
// Require the HDWalletProvider inside our project
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
[...omitted code...]
networks: {
rinkeby:{
provider: () => {
return new HDWalletProvider(
process.env["MNEMONIC"],
'https://rinkeby.infura.io/v3/' + process.env["INFURA_PROJECT_ID"]
);
},
network_id: 4,
gas: 6700000,
gasPrice: 10000000000,
skipDryRun: true,
},
[...omitted code]
},
}
With the Rinkeby configuration complete, the next step is to fund our account with some test ether. Head over to https://faucet.rinkeby.io and follow the steps to fund your account. Make sure that you switch to the Rinkeby testnet on MetaMask and that you have your MetaMask address handy, and ready to use.
Once funds have successfully transferred into your MetaMask account, you are ready to deploy. Use the command:
MyContract $ truffle migrate --network rinkeby
The output will look like this:
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'rinkeby'
> Network id: 4
> Block gas limit: 29970705 (0x1c95111)
1_initial_migration.js
======================
Replacing 'Migrations'
----------------------
> transaction hash: 0xc322889b856b03bc446e10943f3e805b3242e1a227e57f566a82daea57547af5
> Blocks: 2 Seconds: 16
> contract address: 0x23bC90922Db61d0CaD16Dc7eEE1281Be340E94AC
> block number: 9592168
> block timestamp: 1636163584
> account: 0x800705369a9244e399250574B7f8Fa41F81CbcCc
> balance: 7.46991418
> gas used: 250142 (0x3d11e)
> gas price: 10 gwei
> value sent: 0 ETH
> total cost: 0.00250142 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00250142 ETH
2_deploy_myContract.js
======================
Replacing 'myContract'
----------------------
> transaction hash: 0xf9fb0274ce65aada207c3d43fa46589ce58c8b5757d2535c29e7af03d1d0c60d
> Blocks: 1 Seconds: 12
> contract address: 0x0a4044205EEf1058C190B1202c4CE6577712fE1b
> block number: 9592170
> block timestamp: 1636163614
> account: 0x800705369a9244e399250574B7f8Fa41F81CbcCc
> balance: 7.46350893
> gas used: 594612 (0x912b4)
> gas price: 10 gwei
> value sent: 0 ETH
> total cost: 0.00594612 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00594612 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.00844754 ETH
Let’s analyze the output above. As we can see, the contracts have successfully migrated to the Rinkeby public testnet. Two transaction hashes got generated as a result of deploying each contract. Each contract has a address and the account that deployed the contract is identified as0x800705369a9244e399250574B7f8Fa41F81CbcCc.
To verify that our transaction got published to the Rinkeby testnet, let’s copy one of the transaction hashes and head over to https://rinkeby.etherscan.io/. On that page, paste the transaction hash you copied earlier. You should see detailed information about the transaction itself.
If you want to look at all transactions made by a specific account, along with its balance, copy and paste the account address inside the search box.
As we can see, the balance on EtherScan is the same as the one in our MetaMask wallet. We can also see all transactions that were made by 0x800… account
BONUS
How To Deploy Your Smart Contract Using Private Keys Instead Of Mnemonic Phrase
The first thing we need to do is get our private keys from MetaMask. Make sure you are on the Rinkeby test network.
On MetaMask,
- Click on the three dots next to your account. A dropdown will pop up.
- Click on Account Details.
- Click on Export Private Key
- Enter your MetaMask Password
- Copy your Private Keys for later use
Now, we are going to add a new entry inside our .env file.
MNEMONIC="<YOUR_METAMASK_MNEMONIC>"
INFURA_PROJECT_ID="<YOUR_INFURA_PROJECT_ID>"
METAMASK_PRIVATE_KEY="<YOUR_PRIVATE_KEY>"
Inside our truffle-config.js, let’s modify our rinkeby entry to make use of private keys.
// ./truffle-config.js
[...omitted code...]
rinkeby:{
provider: () => {
return new HDWalletProvider(
process.env["METAMASK_PRIVATE_KEY"],
'https://rinkeby.infura.io/v3/' + process.env["INFURA_PROJECT_ID"]
);
},
network_id: 4,
gas: 6700000,
gasPrice: 10000000000,
skipDryRun: true,
},
[..omitted code...]
The deployment should still pass.
One Last Thing
In a previous section, we briefly explored the artifacts ./client/src/contracts/myContract.json file, specifically the networks field which was empty. If we look back at the field now that we have deployed the contract to Rinkeby, we will see a new entry within the networks field of the artifact. This field provides information about the network_id where the contract was deployed (4 for rinkeby in our case), the address of the smart contract, and the transactionHash that was generated as a result of the deployment to this specific network.
It is important to note that frontend applications might need this data to pinpoint where the contract got deployed. The network field provides us with that information.
That’s it! Congratulations, once again. You just deployed your first Solidity Smart Contract to the Rinkeby testnet using the Infura node manager. Go ahead and flex on your friends. Your contract lives on a public testnet.
Conclusion
In this blog post, we learned how to set up our project to work like a blockchain project using the truffle init command. This command generated a boilerplate for us to use. Next, we wrote our first smart contract and associated test. We also went deep into advanced concepts, such as Solidity state variables and functions. We learn how to inherit functionalities from another smart contract, especially the Ownable.sol smart contract from OpenZeppelin, which allowed us to manage ownership of our smart contract. Next, we explored in detail different ways to deploy our contract. We learned how to deploy on a local blockchain like Ganache, and finally, we deployed our contract to a public testnet Rinkeby, with the help of Infura, a service provider for managed Ethereum nodes.
I hope you are ready to take your Solidity skills to the next level with this blog post.
Cheers!