POSTED 3 months ago

Understanding smart contract compilation and deployment

As discussed earlier in the series, when developing dApps, and especially writing smart contracts, there are many repetitive tasks you will undertake. Such as compiling source code, generating ABIs, testing, and deployment.

Development frameworks hide the complexity of these tasks and enable you as a developer to focus on developing your dApp/idea.

Before we take a look at these frameworks such as truffle, embark and populous, we’re going to take a detour and have a look at the tasks performed and hidden by these frameworks.

Understanding whats happening under the hood is particularly useful when you run into issues or bugs with these frameworks.

So this article will walk you through how to manually compile and deploy your Bounties.sol smart contract from the command line, to a local development blockchain.

Steps

Before deployment, a smart contract needs to be encoded into EVM friendly binary called bytecode, much like a compiled Java class. The following steps typically need to take place before a contract is deployed:

  1. Smart contract is written in a human friendly language (e.g Solidity)
  2. The code is compiled into bytecode and a set of function descriptors (Application Binary Interface, known as ABI) by a compiler (e.g Solc)
  3. The bytecode is packed with other parameters into a transaction
  4. The transaction is signed by the account deploying the contract
  5. The signed transaction is sent to the blockchain and mined

So for step 1, will we use the Bounties.sol contract we have written previously in this series.

Solc Compiler

Step 2 requires us to compile our smart contract, in order to compile Solidity we need to use the Solc compiler. Typically frameworks such as truffle, embark and populous come with a version of solc preconfigured, however since we will be compiling without a framework, we will need to install solc manually.

Installing Solc

We can install solc using homebrew:

$ brew update
$ brew upgrade
$ brew tap ethereum/ethereum
$ brew install solidity

Or on ubuntu like so:

$ sudo add-apt-repository ppa:ethereum/ethereum
$ sudo apt-get update
$ sudo apt-get install solc

Read more about installing solc here

One installed double check its installed by checking the compiler version like so:

$ solc --version

solc, the solidity compiler commandline interface
Version: 0.4.24+commit.e67f0147.Darwin.appleclang

Installing JQ

To help with processing json content, during compilation and deployment lets install JQ Using homebrew:

brew install jq

Or on ubuntu like so:

sudo apt-get install jq

Read more about installing jq here

Compiling Solidity

Once solc is installed we can now compile our smart contract. Here we want to generate

  • The bytecode (binary)to be deployed to the blockchain
  • The ABI (Application Binary Interface) which tells us how to interact with the deployed contract

So lets setup our directory to work from and copy our Bounties.sol smart contract

mkdir dapp-series-bounties
cd dapp-series-bounties
touch Bounties.sol

Now copy the contents of Bounties.sol which we previously developed into the Bounties.sol file.

The command to compile would be:

$ solc --combined-json abi,bin Bounties.sol > Bounties.json

This command tells the compiler to combine both the abi and binary output into one json file, Bounties.json

We can view the output using jq:

$ cat Bounties.json| jq

{
"contracts": {
"Bounties.sol:Bounties": {
"abi": "[{\"constant\":false,\"inputs\..."]}",
"bin": "6080604052348015..."
}
},
"version": "0.4.24+commit.e67f0147.Darwin.appleclang"
}

The above output has been trimmed down since the abi and bin outputs are quite large, however from the output above we can see the structure of the compiled json output:

  • The name of the Solidity file and the name of the contract specified within it Bounties
  • The ABI
  • The bytecode (bin)
  • The version of the compiler

Development Blockchain: Geth & Ganache-CLI

In order to deploy our smart contract we’re going to need an Ethereum environment to deploy to. For this, we will use Ganache-CLI to run a local development blockchain environment.

Installing Ganache-CLI

$ npm install -g ganache-cli

Installing Geth

We will also install Geth, which is the Go implementation of an Ethereum node. You can run Geth to setup your own private network by following these instructions. However, we will only be using Geth in this tutorial as a helper since it provides a nice Javascript console environment with web3 installed which we can use to interact with Ganache-CLI.

Using homebrew:

$ brew tap ethereum/ethereum
$ brew install ethereum

Or on ubuntu like so:

$ sudo apt-get install software-properties-common
$ sudo add-apt-repository -y ppa:ethereum/ethereum
$ sudo apt-get update
$ sudo apt-get install ethereum

Read more about installing Geth here

Starting Ganache-CLI & attaching Geth Console

So lets start our local development blockchain environment:

$ ganache-cli

Ganache CLI v6.1.3 (ganache-core: 2.1.2)
Available Accounts
==================
(0) 0x11541c020ab6d85cb46124e1754790319f6d8c75
(1) 0xc44914b445ced4d36f7974ec9b07545c6b39d58d
(2) 0x443078060573a942bbf61dcdae9c711c9e0f3426
(3) 0x509fc43d6b95b570d31bd1e76b57046131e815ab
(4) 0xaf3464e80e8981e868907e39df4db9116518b0b8
(5) 0x9894e6253606ee0ce9e0f32ae98cacf9cedc580c
(6) 0x8dc4480b3d868bbeb6caf7848f60ff2866d5e98d
(7) 0x5da85775ca3cdf0048bff35e664a885ed2e02ff7
(8) 0x1acc13d7d69ac44a96c7ee60aeee13af6b001783
(9) 0x9c112d3a812b47909c2054a14fefbbb7a87fb721
Private Keys
==================
(0) 08f8aaea81590cea30e780666c8bdc6a3a17144130dcf20b07b55229b2d5996b
(1) b8ef92de39bcaf83eb7622ba62c2dd055f0d0c62053ab381aa5902fdd8698f91
(2) 8d0a626a420f68c6a1c99025fe4c17e02b8853feefd82a908bebdb994c589e31
(3) 7d6a122d935f9244b47919a24e218d9bb6d54eff63de5eb120405e3194bf7658
(4) 738d3ddcd659cc45ddf4044bc512ff841717af3cd0f27f77338bc759d6a9769d
(5) e9f82c125a8b9ca386b7cd59101ba4105a7c25d30727fdb937391798a01211ef
(6) 2c70bd342bf610cbc974b24ec6f11260cebd537cdde65d7922971a7d4858cc5b
(7) 8f27ce51b5b4784a75cddc2428861dc07c3dd4ceac81c2f32eb4d8f86ff51ca0
(8) 377ab95e5c5fbe97f8a298b4a108062b063e9ce5fa7e513691494f5458419f7a
(9) 4919c7b8934160a1ec197cf19474d326486d63ced25dfb65f0a692bdba3d2208
HD Wallet
==================
Mnemonic: attend frost dignity wheat shell field comic tooth include enter border theory
Base HD Path: m/44'/60'/0'/0/{account_index}
Gas Price
==================
20000000000
Gas Limit
==================
6721975

Listening on localhost:8545

The above output shows ganache-cli has started and is listening on localhost:8545

Now lets attach the Geth Javascript console to our environment:

$ geth attach http://localhost:8545

Welcome to the Geth JavaScript console!
instance: EthereumJS TestRPC/v2.1.2/ethereum-js
coinbase: 0x11541c020ab6d85cb46124e1754790319f6d8c75
at block: 0 (Mon, 13 Aug 2018 12:35:46 BST)
modules: eth:1.0 evm:1.0 net:1.0 personal:1.0 rpc:1.0 web3:1.0
>

Deployment preparation

In order to deploy we first need to load our compiled json output into our Geth console.

In a separate terminal, we’ll first prepare a .js script which we can load into the Geth console.

Navigate to the directory where the compiled Bounties.json output is located.

Run the following:

$ echo "var bountiesOuput=" > Bounties.js
$ cat Bounties.json >> Bounties.js

This should setup a variable named bountiesOuput which is equal to the compiled json file we produced earlier.

$ cat Bounties.js

var bountiesOutput=
{"contracts":{"Bounties.sol:Bounties":{
"abi":"[{\"constant\":false,\"inputs\":[{\"name\":\"_bountyId\",\"type\":\"uint256\..."}]"}]",
"bin":"608060405234801561001057600080fd5b506..."}},
"version":"0.4.24+commit.e67f0147.Darwin.appleclang"}

Note: the output above has been trimmed down since the abi and bin outputs are quite large.

Now in our Geth console terminal, we run the following to load the Bounties.js script and thus the bountiesOuput variable into our console environment:

> loadScript('Bounties.js')
true
> bountiesOutput
{
contracts: {
Bounties.sol:Bounties: {
abi: "[{\"constant\":false,\"inputs\":[{\"name\":\"_bountyId\",\"type\":\"uint256\..."}]"}],
bin: "608060405234801561001057600080fd5b506..."
}
},
version: "0.4.24+commit.e67f0147.Darwin.appleclang"
}

Let’s prepare the object that will help us interact with the contract. We give it the ABI that, among other things, defines the available methods. However, since the ABI interface was created in stringified form, we need to parse it first:

> typeof bountiesOutput.contracts["Bounties.sol:Bounties"].abi
"string"
> var bountiesContract = eth.contract(JSON.parse(bountiesOutput.contracts["Bounties.sol:Bounties"].abi));
undefined
> bountiesContract
{
abi: [{
constant: false,
inputs: [{...}, {...}],
name: "fulfillBounty",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function"
}, {
constant: false,
inputs: [{...}, {...}],
name: "issueBounty",
outputs: [{...}],
payable: true,
stateMutability: "payable",
type: "function"
}, {
....
at: function(address, callback),
getData: function(),
new: function()

bountiesContract is our web3 contract object, we take a further look into web3 later on in this series. This web3 contract object is not an instance, it is a factory class which can be used to create contract instances with the .at() and .new() functions.

Lets also now prepare the bin (compiled binary bytecode) into a variable. Notice that in order to have the expected hex value we need to add "0x" to the beginning.

> var bountiesBin = "0x" + bountiesOutput.contracts["Bounties.sol:Bounties"].bin
undefined
> bountiesBin
"Ox608060405234801561001057600080fd5b50611040806100206000396000f30060806040526004361061006d576000357c01000000000000000000000000000000000000000...."

Deployment

Now its time to deploy our smart contract!

We first construct our deployTransactionObject:

> var deployTransationObject = { from: eth.coinbase, data: bountiesBin, gas: 3000000 };
undefined

We set the following paramaters in our transaction object.

  • from: eth.coinbase: the transaction should be sent from the coinbase account
  • data: bountiesBin: set the binary of our compliled contract as the data input of our transaction
  • gas: 3000000: we set enough gas for our contract to be deployed

We then invoke our BountiesContract factory function .new() passing in our deployTransationObject as an argument.

> var bountiesInstance = bountiesContract.new(deployTransationObject);
> bountiesInstance
{
abi: [{
constant: false,
inputs: [{...}, {...}],
name: "fulfillBounty",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function"
}, {
constant: false,
inputs: [{...}, {...}],
name: "issueBounty",
outputs: [{...}],
payable: true,
stateMutability: "payable",
type: "function"
}, {
...}],
address: undefined,
transactionHash: "0xbeee5a5db59504c289e30a9843d8bf05bd0dcd66831993fde6a3986e2f022a52"
}

So bountiesInstance is a web3 object which represents the bounties contract instance deployed to our development environment.

Depending on the version of Geth you are using, you may or may not have the address field set, in the contract instance. Usually this will be set when the transaction to deploy the contract has been mined.

If not, as above address field is set to undefined, we will have to manually set the contract address

We can find the contract address by viewing the transaction receipt of the deploy contract transaction. Our bountiesInstance object will have a reference to the transactionHash.

eth.getTransactionReceipt(bountiesInstance.transactionHash);
{
blockHash: "0x565c4ebb83d9036518c33634d425f37b9ef3aa125e9b09386fbbdd44099892d9",
blockNumber: 1,
contractAddress: "0x1f81f1dd0de1670eac0bfa9e00a854733470d646",
cumulativeGasUsed: 911612,
gasUsed: 911612,
logs: [],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
status: "0x1",
transactionHash: "0xbeee5a5db59504c289e30a9843d8bf05bd0dcd66831993fde6a3986e2f022a52",
transactionIndex: 0
}

We can see the contract address above is: “0x1f81f1dd0de1670eac0bfa9e00a854733470d646”

We can save this as a variable for later use:

> var bountiesAddress = eth.getTransactionReceipt(bountiesInstance.transactionHash).contractAddress;
undefined

> bountiesAddress
"0x1f81f1dd0de1670eac0bfa9e00a854733470d646"

We now override our existing bountiesInstance object using the .at() function of the bountiesContract factory to create a bountiesInstance pointing at the correct contract address:

> var bountiesInstance = bountiesContract.at(bountiesAddress)
undefined
> bountiesInstance
{
abi: [{
constant: false,
inputs: [{...}, {...}],
name: "fulfillBounty",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function"
}, {
constant: false,
inputs: [{...}, {...}],
name: "issueBounty",
outputs: [{...}],
payable: true,
stateMutability: "payable",
type: "function"
}, {
...}],
address: "0x1f81f1dd0de1670eac0bfa9e00a854733470d646",
transactionHash: null,
BountyCancelled: function(),
BountyFulfilled: function(),
BountyIssued: function(),
FulfillmentAccepted: function(),
acceptFulfillment: function(),
allEvents: function(),
bounties: function(),
cancelBounty: function(),
fulfillBounty: function(),
issueBounty: function()

Above now you notice the bountiesInstance object now references the correct contract address: “0x1f81f1dd0de1670eac0bfa9e00a854733470d646”

Also, we can see the list available functions which are provided by our smart contract.

Interacting with our contract

In our console, it is also possible to interact with our Bounties contract.

Lets attempt to issue a bounty. To do this we’ll need to set the string _data argument to some string “some requirements” and set the uint64 _deadline argument to a unix timestamp in the future e.g “1691452800” August 8th 2023.

> bountiesInstance.issueBounty("some requirements","1691452800",{ from: eth.accounts[0], value: web3.toWei(1, "ether"), gas: 3000000 });
"0xb3a41fa36c09010abbfa9bf80c3cd11242e4476506d6bf8b363b8feeb3cf946d"

> eth.getTransactionReceipt("0xb3a41fa36c09010abbfa9bf80c3cd11242e4476506d6bf8b363b8feeb3cf946d")
{
blockHash: "0x8acd35b251c3f1c23a6276540e73b958de28686a89367ed108bef9f614771099",
blockNumber: 5,
contractAddress: null,
cumulativeGasUsed: 133594,
gasUsed: 133594,
logs: [{
address: "0x1f81f1dd0de1670eac0bfa9e00a854733470d646",
blockHash: "0x8acd35b251c3f1c23a6276540e73b958de28686a89367ed108bef9f614771099",
blockNumber: 5,
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c74a4fba809c8f0e6b410b349f2908a4dbb881230000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000011736f6d6520726571756972656d656e7473000000000000000000000000000000",
logIndex: 0,
topics: ["0xba1576d8891bfe57a45ac4b986d4a4aa912c62f44771d4eec8ab2ce06e3be5b7"],
transactionHash: "0xb3a41fa36c09010abbfa9bf80c3cd11242e4476506d6bf8b363b8feeb3cf946d",
transactionIndex: 0,
type: "mined"
}],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000040000000000000000000000000000004000000000000010000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
status: "0x1",
transactionHash: "0xb3a41fa36c09010abbfa9bf80c3cd11242e4476506d6bf8b363b8feeb3cf946d",
transactionIndex: 0
}

Above is log output of both the transaction to issue a bounty and the transaction receipt showing the transaction has been successfully mined.

Because this was the first bounty issued, we know from our contract code that the issueBounty function will create this bounty with bountyId of 0.

Also if we inspect the data element of the logs element from the results of the transaction receipt. We will see that the first part of the data ouput is indeed the bountyId of our newly issued bounty:

logs: [{
address: "0x1f81f1dd0de1670eac0bfa9e00a854733470d646",
blockHash: "0x8acd35b251c3f1c23a6276540e73b958de28686a89367ed108bef9f614771099",
blockNumber: 5,
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c74a4fba809c8f0e6b410b349f2908a4dbb881230000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000011736f6d6520726571756972656d656e7473000000000000000000000000000000",
logIndex: 0,
topics: ["0xba1576d8891bfe57a45ac4b986d4a4aa912c62f44771d4eec8ab2ce06e3be5b7"],
transactionHash: "0xb3a41fa36c09010abbfa9bf80c3cd11242e4476506d6bf8b363b8feeb3cf946d",
transactionIndex: 0,
type: "mined"
}]

Now we know the bountyId, we can call the bounties function of our bountiesInstance web3 object to double check the issueBounty function stored our data correctly:

> bountiesInstance.bounties.call(0)
["0xc74a4fba809c8f0e6b410b349f2908a4dbb88123", 1691452800, "some requirements", 0, 1000000000000000000]

Here we confirm from the output that our bounty with bountyId=0 has the following data:

  • Issuer: “0xc74a4fba809c8f0e6b410b349f2908a4dbb88123”
  • deadline: 1691452800
  • data: “some requirements”
  • bountyId: 0
  • amount: 1000000000000000000 (Weis)

Success!

There you have it, we have successfully:

  • Compiled out Bounties.sol smart contract
  • Deployed it to a local development blockchain
  • Issued a bounty by sending a transaction
  • Checked out bounty data by making a call to the contract

Now that we have done it the hard way, the next part of the series will look at home development frameworks such as truffle, embark and populous hide the complexity of this process from us, and allow us to focus on writing smart contracts when developing our dApps!