James Bachini

Solidity Tutorial | For Developers Coming From Javascript, C++, Python

solidity tutorial

In this Solidity tutorial I’ll be going through the work flow to develop smart contracts on Ethereum using the Solidity programming language.

  1. Solidity Developer Tutorial [Video]
  2. Introduction To Solidity Smart Contracts
  3. Solidity Dev Environment
  4. Hello World Example
  5. Variables & Data Handling
  6. Solidity Contract Layout & Flow
  7. Useful Solidity Code Snippets
  8. Testing, Auditing & Security
  9. Migrating On-Chain With Truffle
  10. Conclusion

Solidity Developer Tutorial [Video]

James On YouTube

Introduction To Solidity Smart Contracts

Solidity is the programming language of Ethereum and many other blockchains that use the Ethereum virtual machine.

Solidity Logo

A developer will write code which forms a smart contract. The contract will be migrated on to the Ethereum blockchain where it will be allocated an address. We can then interact with the external functions of that smart contract using it’s Ethereum address or more usually a web based frontend user interface.

Solidity code can not be updated or altered once deployed. This means it’s not possible to fix bugs or constantly develop a contract. In practice this means that code needs to be rigorously tested and audited before deployment. Developers will often release new versions of existing code such as Uniswap v3 but these are a completely new set of contracts and funds and users will need to be migrated across to the new smart contracts.

Smart contract code is executed on a decentralised network of computers around the world which form the Ethereum network. Code must always execute to the same state result across multiple instances so a consensus on the output can be achieved.

If you’ve ever used a dApp (decentralised application) such as Uniswap or Yearn Finance then you have interacted with smart contracts via a web based user interface which uses a Javascript library called web3.js to connect to the Ethereum network.

So in decentralised applications Solidity smart contracts form the backend application layer. Small amounts of data can also be stored on-chain in state variables. Deploying a smart contract and executing write functions which change the state are relatively expensive and a gas fee will need to be paid. Read functions which do not alter the state of the contract are free.

Solidity Dev Environment

There are some tools and applications we are going to need to get started with Solidity.

The first is simply to open a web browser window with https://remix.ethereum.org

Remix is a browser based EVM (ethereum virtual machine) simulator that is useful for developing and testing solidity smart contracts.

Then we are going to need the following tools:-

  • NodeJS – Javascript run time environment used to deploy contracts
  • Truffle – EVM development and testing framework
  • Ganache – Locally host a EVM blockchain for testing

NodeJS and Ganache can be installed by downloading their application. Once NodeJS is installed, reset the machine and then open up a command prompt and install truffle with:

npm install -g truffle

You should end up with something like this

Solidity Tutorial Dev Environment

There are some additional tools that you may require:-

  • VSCode – Text editor if you don’t have one already, there are plenty of Solidity extensions including the one I made.
  • Web3.js – Front end framework
  • Infura API Key – Access point for the Ethereum network

Hello World Example

Let’s write our first smart contract to store “Hello World” on the blockchain

First go to https://remix.ethereum.org and select file explorer from the left hand menu, if it’s not already open.

Navigate to the contracts directory and then click the New File icon which looks like a blank page.

Name the file HelloWorld.sol and then copy and paste the following code into the text editor.

pragma solidity >=0.8.0;

contract HelloWorldContract {
  string public myStateVariable = 'Hello World';
}

Line 1 declares the version of Solidity I am using. We then setup the contract and give it a name HelloWorldContract. Finally we set a state variable (similar to a global variable in other programming languages) which is stored on the blockchain. State variables can be modified via external functions but in this example we are just setting it to a text string ‘Hello World’. When we define a public variable in this way a getter function is automatically created during the compilation so we can access the variable via an external function.

Let’s go ahead, compile and deploy this code within Remix. So click the next tab down on the left menu Solidity Compiler and click the big blue button Compile HelloWorld.sol

Then if there are no errors move to the next tab down Deploy & Run Transactions. Click the big orange Deploy button and a contract should be added to the bottom left Deployed Contracts container. Expand the contract and click the myStateVariable button and we should get a “Hello World” result.

Solidity Hello World Example

Admittedly this is running in a local browser and not on a decentralised network at this stage but the code is working and we can test functions and do quite a lot just from within the Remix IDE. Later on in this tutorial we will look at how to migrate this contract on to the Ethereum blockchain.

In the solidity tutorial video I added a constructor function which executes on deployment and a updateVar public function to update the variable. The full code is below:

pragma solidity >=0.8.4;

contract HelloWorldContract {
  string public myStateVariable;

  constructor() {
    myStateVariable = 'Test 1';
  }

  function updateVar() public {
    myStateVariable = 'Test 2';
  }
}

Variables & Data Handling

Solidity variables can be separated into two key groups:

  1. State variables are a little bit like global variables and are stored on the blockchain. They can be modified and updated after migration and their current state is stored in a decentralised manner.
  2. Local variables are specific to a function and are stateless. They are initialised the same every time the function executes.
int public myStateVariable = 1;

function getVar() public pure returns(int) {
  int myLocalVariable = 1;
  return myLocalVariable;
}

By adding public to the state variable in line 1 we create a function of the same name which can get the value of that variable from the blockchain. We saw this earlier in the hello world example.

Data Types

Solidity is statically typed which means we can’t do weird stuff like change an integer variable to a string as you can in Javascript. Once we have set a data type for a variable it is static for the entire execution of that smart contract. Here are some examples of regularly used data types:

  • string = text
  • address = ethereum address (no quotes required)
  • int = integars
  • uint = unsigned integar (no negatives)
  • bool = boolean true/false

We can optimise size by defining bit size.
uint8 vs uint256
string vs bytes32

If you are coming from a loosely typed programming language such as Javascript just bare in mind that we need to define the type when creating a variable similar to Typescript.

Access Modifiers

Access modifiers are used when defining a variable or function and limit the accessibility of that value.

  • public – Publicly accessible variable internally and externally. An automatic getter function is created for public variables.
  • private – Private value accessible only within scope of contract
  • internal – only accessible internally from within the contract
  • external – can only be called via other contracts or external function calls

State Modifiers

State modifiers can restrict the access a function has to the state within a smart contract.

  • undefined – if we don’t declare a state modifier then there are no restrictions on state access
  • constant – can not access state, blockchain or execution data
  • view – view state variables but not modify
  • pure – no access to view or modify state
  • immutable – assigned at construction and unchangeable there after

Arrays

We can turn a data type into an array of a specified data type like this: (Notice the [] after the data type declaration.)

string[] public favourites = ["James","Peter","Simon"];
favourites.push("Tom");
return favourites.length;

Struct & Mapping

We can create custom data type structures which are similar to how classes are used to organise data in C++;

struct Car {
  string manufacturer;
  uint year;
  string model;
}

Often this is combined with mapping to create a key value type of data store. Here we use the Car structure we defined and give it a uint (unsigned integer or numerical) id reference. The addCar function creates a instance of the Car structure with a set id reference.

mapping(uint => Car) public cars;

function addCar(uint _id, string memory _manufacturer, uint _year, string _model) public {
  cars[_id] = Car(_manufacturer, _year, _model);
}

Solidity Contract Layout & Flow

Bugs in Solidity code can have devastating financial implications, for this reason most developers will rely on audited code from 3rd parties such as Open Zeppelin.

Solidity uses inheritance to import external code like this example where we create a commonly used ERC20 token.

pragma solidity >=0.8.0;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
  constructor () public ERC20("Token", "JBTOK") {
    _mint(msg.sender, 100000 * (10 ** uint256(decimals())));
  }
}

Conditions & Loops

Conditions and loops are fairly straight forward and should be familiar to devs coming from other programming languages. Here are some examples showing if, for and while.

if (myVar > 2) {
  return true;
} else {
  return false;
}

for (i = 0; i < 10; i++) {
  repeatSomething();
}

bool check = false;
while (check == false) {
  check = true;
}

Useful Solidity Code Snippets

In this section I’ll go through some code snippets that will be useful and help learn along the way.

Often for security reasons we will want to restrict access to certain functionality within a contract. We can set the owner address variable to the creator of the contract. This is set to the deployers Ethereum address using a constructor function which is executed only on initial deployment.

address public payable owner;

constructor() public {
  owner = msg.sender;
}

function isItMe() public {
  require(owner == msg.sender, "Not Authorised!");
  doSomething();
}

We can map structures to addresses which is used widely to define user variables for the person interacting with the contract. In this simplified example we just use a constructor function to set the values for the owners address.

struct Stakers {
    string name;
    uint amount;
}

mapping(address => Stakers) public stakeHolders;

constructor() {
    stakeHolders[msg.sender].name = 'James';
    stakeHolders[msg.sender].amount = 5;
}

ETH funds can be transferred both to and from a contract. msg.sender is the address interacting with the external function of the contract.

payableAddress.transfer(msg.value);
msg.sender.transfer(this.balance);

Timing is slightly complicated because of the need to keep state the same across the network. The closest we can get is to either use a block number or last completed block time. The Ethereum network will create a new block roughly every 13 seconds.

uint public blockNo = block.number;
uint public recentTimestamp = block.timestamp;

We can execute functions on external contracts by importing their solidity code and providing the external contract address during migration. This is another example of using the constructor function to execute code only at migration.

import "./ExternalContract.sol";

contract MyContract {
  Storage public s;

  constructor(ExternalContract addr) public {
    s = addr;
    calculateResult = 0;
  }
  
  function saveValue(uint x) public returns (bool) {
    s.setValue(x);
    return true;
  }
}

Testing, Auditing & Security

Combine the fact solidity smart contracts can’t be modified once they have been deployed with the seventy billions of dollars currently contained within the DeFi ecosystem on Ethereum and it’s clear testing and auditing is critical.

NodeJS can be used to create unit tests using npm test frameworks such as Chai. Here’s a simplified example:

require('chai').use(require('chai-as-promised')).should();

contract('MyTestContract', ([owner]) => {
  describe('Test deployment', async () => {
    it('Check owner balance', async () => {
      result = await myToken.balanceOf(investor);
      assert.equal(result.toString(), tokens('10000'), 'owner token balance correct')
    })
  })
}

Testing should be as thorough as possible to ensure each function works as expected.

Any code handling financial transactions or investment should be audited by a 3rd party auditor where possible.

Some leading names in this field are:-

Code audits are relatively expensive but not as expensive as a exploit or locked funds.

Open sourcing the code and asking for other developers to look over the code is another way to get peer review and another pair of eyes that might spot something overlooked by the original developer.

Solidity Security – 6 Common Vulnerabilities

Here are 5 common Solidity security issues that developers should know about.

  1. Inflatable Loops – If a user can add multiple addresses to an array which is then looped over to distribute funds, for example, an attacker could add enough addresses to make it impossible to loop through the array without exceeding the block gas limit. This DoS attack locks funds in the contract unless there is a back up function to recover them.
  2. Reentrancy – Avoid external function reentrancy by sending funds to external addresses only after all state changing logic has been executed and using the built-in transfer function where possible.
  3. Variable & Function Access – Limit public functions and variables to only those that are absolutely necessary to the application. The more doors there are into a smart contract the more chances you give an attacker to find an exploit.
  4. Flash loan attacks – Commonly used to exploit liquidity pool contracts that don’t use an oracle as a way to provide a “reasonable price”. An attacker can borrow a huge amount of ETH for a single block to unbalance a pool and drain it of funds.
  5. Frontrunning Bots – Just because you send a transaction to the blockchain first doesn’t mean it necessarily gets executed first. Frontrunning has become big business and bots listen out for large orders on DEX’s and then place the same trade with a higher gas fee to get ahead of the original order.
  6. Integer Overflow – Because memory space is of a premium on the blockchain devs will often limit integar sizes to a specific byte size. If checks aren’t in place then these values can be exceeded causing the integar to contain an incorrect value.

Migrating On-Chain With Truffle

Smart contracts deployments are called migrations and developers use automated scripts to migrate their contracts on chain. The general work flow for Solidity development is as follows:

So once you’ve got a contract running in Remix it’s time to deploy it on-chain to a testnet before going live on Ethereum mainnet. A testnet can either be set up locally or you can use a remote testnet. We set up a local testnet earlier when we installed Ganache, this runs a copy of Ethereum on our local machine. Remote testnets are a network of nodes which operate in exactly the same way as the mainnet nodes. The main difference is that ETH on a testnet has no real value and you can get it for free to break stuff with.

There are a number of different remote Ethereum testnets. I tend to use Ropsten or Kovan and there is very little difference between them. You can get free testnet ETH to pay for the migration by googling “Ropsten faucet” and putting your public key in to the faucet.

Metamask Rospten

You can also use Metamask with the Ethereum testnets by selecting the network at the top of the browser extension or adding a custom RPC for localhost. This can be quite handy for moving tokens around and testing out user interfaces.

You’ll also need an Infura API key for the Ropsten testnet and the Ethereum mainnet for that matter. Infura acts as a node on the Ethereum network which you can connect to and transmit transactions. API keys are free for general usage up to 100,000 requests per day https://infura.io/docs/gettingStarted/chooseaNetwork

So if we run the following command from a shell:

truffle init

This will set up a standard directory structure with contracts, test, migrations

Setting Up Truffle For Solidity Development

Add the infura API key to the truffle-config.js file and go through the well commented options in that file to setup your environment and testnet target. There are plenty of working examples on github to copy from if something doesn’t make sense but here’s the important bits:

const HDWalletProvider = require('@truffle/hdwallet-provider');
const infuraKey = "123";
const mnemonic = "abc";

ropsten: {
  provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/${infuraKey}`),
  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 )
},

Note the line starting provider: has a quirk that you need to manually add the infuraKey variable or value in the current version.

You’ll need a mnemonic which is used to generate a private public key pair, this is essentially your ethereum address or account. For testnet you can use the mnemonic at the top of Ganache then send funds from a faucet to the first public key listed. Don’t use this address for mainnet and real funds.

We can then add our solidity files from remix to the contracts directory and unit tests to the tests directory.

In the migrations directory there will already be a file called 1_initial_migration.js so let’s go ahead and create a second one 2_deploy_contracts.js we can set this up to migrate the hello world contract we created earlier. We can alternatively use the truffle command line truffle create migration deploy_contracts

const HelloWorldContract = artifacts.require("HelloWorldContract");
module.exports = function(deployer) {
    deployer.deploy(HelloWorldContract);
};

The first line here includes the contract abstraction and stores it in the HelloWorldContract variable. We are then exporting a module function which includes the built-in deployer for our contract.

So once you have some testnet ETH from a faucet, an Infura API key to connect to and the configuration setup it’s time to push it out.

npm install @truffle/[email protected]
truffle migrate --network ropsten

I’m installing version 1.2.2 of the hdwallet-provider as the most recent version has a couple of issues which will likely be fixed by the time you read this and you can install the latest version by removing @1.2.2

If all goes well you should see something like this. You can take the contract address and plug it into Ropsten Etherscan. Here is my helloworld contract in all it’s glory: https://ropsten.etherscan.io/address/0x723d07ce03b21F58ECf6e52dC4901B1dE72b139b

Solidity Contract Tutorial

We can now interact with the contract like this:-

truffle console --network ropsten
const hi = await HelloWorldContract.deployed();
// return undefined
hi.myStateVariable();
// returns "hello world"

It’s also possible to interact with a contract at a specific address

HelloWorldContract.at('0x723d07ce03b21f58ecf6e52dc4901b1de72b139b').myStateVariable();

We can also verify the source code and play with the contract directly on Etherscan:

Solidity Example Tutorial

To migrate to the mainnet we just edit the truffle-config.js file to add the main Ethereum network which has a network_id of 1. Repeat the steps and it should work exactly the same.


Conclusion

Solidity is the most widely used programming language for smart contracts. With 3rd parties like Binance Smart Chain and Avalanche adopting the Ethereum virtual machine as well it seems this will remain the case for the foreseeable future.

Developing Solidity code is different to other programming languages and development work flows. There is zero room for error or bugs which makes it more like hardware development than software. More time goes into planning and testing and even the simplest of functions can take a day to get perfect and test.

This also, rightly or wrongly, incentivises the use of existing code which you could argue hinders creativity. DeFi has been described as the lego bricks of finance and I find myself mashing up and working with other devs contracts and writing connecting code or extensions a lot of the time. The less new code, the less chance there is of bad things happening, in theory.

If you are going to write code that is going to be used for financial transactions then get an audit if possible. Having a second pair of eyes look over the contract from a purely security perspective is invaluable. Read the public audits for popular DeFi protocols and see what they got picked up on because there’s almost always something in every report to learn from.

I hope this tutorial has proved valuable and wish you all the best in your journey to developing smart contracts.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.