web3 tutorial [03/10] - solidity: minutely rate smart contract

by parttimelarry

What is Solidity?

Solidity is a programming language for writing smart contracts on Ethereum. It has syntax similar to JavaScript by design. If you are a web developer, Solidity programs will look vaguely familiar.

Unlike JavaScript, Solidity is statically typed. This means you must specify variable types so that they are known at compile time. Statically typed languages are a bit more verbose and typically take longer to plan, write, and compile. However, this upfront time investment reduces bugs in production, which is especially important when dealing with money.

Our First Contract

Let’s write our own smart contract. We are building Calend3, a web3 calendar dapp. So let’s create a new file called Calend3.sol in the contracts/ directory of our project and add the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    // million dollar dapp idea goes here
}

Now let’s compile the code with hardhat. Open a terminal window. Make sure you are in the top level directory of your project. Enter the following command:

npx hardhat compile

You should see the following output:

Compiling 1 file with 0.8.4
Solidity compilation finished successfully

Congratulations! You’ve just written and compiled a smart contract. You smart. This program doesn’t do much yet, but the journey of a thousand miles begins with a single step. Let’s talk about this program line by line.

What is a contract?

If you’ve programmed in Java or Python, you have probably used a class. In Solidity, we use the word contract. A class is just a collection of state and behavior. In other words, it is a collection of data and functions that operate on that data. Persistent data is stored in state variables. Functions can retrieve and modify these variables.

In one of my previous tutorials, I mentioned that I think of a smart contract like a stored procedure in a relational database. In a database like PostgreSQL or MySQL, you can write database logic in stored procedures. The code for a stored procedure is actually stored in the database. Stored procedures can be used to centralize business logic and enforce data integrity.

Likewise, the code for smart contracts is stored on the blockchain and enforce changes of state to the blockchain.

License

SPDX, or Software Package Data Exchange, is an open standard for specifying software licenses. The official website has a list of licenses you can use in your projects. I typically use the MIT license since it is familiar to me and any code I write for a tutorial is free to use and modify.

Pragma

We use the pragma keyword to control the compilation process. This prevents compiling with incompatible Solidity versions. The line pragma solidity ^0.8.0; means that this program will not compile with versions earlier that 0.8. It also specifies that the code will compile in later versions of Solidity as long as there are no breaking changes. New versions of Solidity could behave differently. For example, a function could be deprecated or best practices could change.

Setting a Rate

Okay, our contract doesn’t do anything yet. Let’s change that by adding some variables and functions. We will intentionally (and unintentionally) make some mistakes along the way so that we learn what can go wrong.

Variables and Functions

Since we are making an appointment scheduler that allows the owner to charge money, let’s allow the owner of the calendar to set their rate. We’ll store the rate as an unsigned integer.

An unsigned integer is specified using uint and means that this integer will always be positive. We expect people to charge a non-negative rate. We’ll also define some functions to get and set this rate. Let’s try to compile the code below:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    uint rate;

    function getRate() {
        return rate;
    }
    
    function setRate(_rate) {
        rate = _rate;
    }
}
➜  npx hardhat compile
Compiling 1 file with 0.8.4
SyntaxError: No visibility specified. Did you intend to add "public"?
 --> contracts/Calend3.sol:7:5:
  |
7 |     function getRate() {
  |     ^ (Relevant source part starts here and spans across multiple lines).


SyntaxError: No visibility specified. Did you intend to add "public"?
  --> contracts/Calend3.sol:11:5:
   |
11 |     function setRate(_rate) {
   |     ^ (Relevant source part starts here and spans across multiple lines).


DeclarationError: Identifier not found or not unique.
  --> contracts/Calend3.sol:11:28:
   |
11 |     function setRate(_rate) {
   |                      ^^^^^^^^^^^


Error HH600: Compilation failed

Oh my god. Lots of errors. Massive fail. That’s okay. We get knocked down, but we get up again. Let’s fix these errors one by one and talk about them along the way.

Public and Private

The first error we encountered was SyntaxError: No visibility specified. Did you intend to add "public"?

This error occurred because we must specify whether each variable or function has public or private access. A public variable or function can be accessed from outside of the smart contract. A private variable or function can only be accessed or called from within the same smart contract.

Since we ultimately want to expose the rate to a web interface and let the calendar owner set it, we should make the getRate() and setRate() functions public.

The second error we encountered was DeclarationError: Identifier not found or not unique.. This is because we didn’t specify the data type in the function signature. To fix this, we just specify that _rate is an unsigned integer by adding uint in front.

Let’s update our code to fix these errors and then try to compile again:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    uint rate;

    function getRate() public {
        return rate;
    }
    
    function setRate(uint _rate) public {
        rate = _rate;
    }
}
calend3 npx hardhat compile
Compiling 1 file with 0.8.4
TypeError: Different number of arguments in return statement than in returns declaration.
 --> contracts/Calend3.sol:8:9:
  |
8 |         return rate;
  |         ^^^^^^^^^^^^
Error HH600: Compilation failed

Oh snap! Failed again. Programming is hard.

Return Values

We got past the first set of errors, but now we have a new problem: TypeError: Different number of arguments in return statement than in returns declaration.. In Solidity, not only do we need to specify the data type for our inputs, we also need to specify the data type for the values we return. I told you statically typed languages are more verbose and take longer to write! Let’s add returns (uint) to our program and try again:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    uint rate;

    function getRate() public returns (uint) {
        return rate;
    }
    
    function setRate(uint _rate) public {
        rate = _rate;
    }
}
➜ npx hardhat compile
Compiling 1 file with 0.8.4
Warning: Function state mutability can be restricted to view
 --> contracts/Calend3.sol:7:5:
  |
7 |     function getRate() public returns (uint) {
  |     ^ (Relevant source part starts here and spans across multiple lines).

Solidity compilation finished successfully

Not again! At least this one isn’t an error. Notice that we received a warning, but the compilation finished successfully. While we can technically ignore warnings in our programs and even in real life, it is probably best to listen and adjust our behavior.

View Functions

A function can be specified as a view to ensure it doesn’t modify any state. Since our getRate() function does not modify any state, we can specify that is is a view function.

Why would we want to do this? What is the benefit? Well, let’s say we are working on a big team of engineers and a new engineer works on this codebase. For some reason they decide to add some additional logic to the getRate() function that doubles the rate set by the user, and they try to compile the program.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    uint rate;

    function getRate() public view returns (uint) {
        rate = rate * 2;
        return rate;
    }
    
    function setRate(uint _rate) public {
        rate = _rate;
    }
}
➜  calend3 npx hardhat compile
Compiling 1 file with 0.8.4
TypeError: Function declared as view, but this expression (potentially) modifies the state and thus requires non-payable (the default) or payable.
 --> contracts/Calend3.sol:8:9:
  |
8 |         rate = rate * 2;
  |         ^^^^

Error HH600: Compilation failed

They will get a compilation error indicating that the state can not be modified in a view function. When writing smart contracts, there is a cost with modifying state: gas. So we want to specify our intent with the view keyword here. This code compiles successfully without warnings:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    uint rate;

    function getRate() public view returns (uint) {
         return rate;
    }
    
    function setRate(uint _rate) public {
        rate = _rate;
    }
}

Contract Ownership

So far we have created a basic smart contract and have written some functions to get and set a rate. Soon we will add functions for managing appointments. But whose appointments? Whose rate are we setting? Can anyone in the world set the rate? Probably not. Only the owner should have permission to configure the calendar.

Accounts

In order to control access to the calendar, we need to specify who owns it. It should probably be owned by a particular Ethereum account. What is an Ethereum account?

Straight from the Ethereum documentation:

An Ethereum account is an entity with an ether (ETH) balance that can send transactions on Ethereum. 
Accounts can be user-controlled or deployed as smart contracts.

Ethereum has two account types:

Externally-owned – controlled by anyone with the private keys
Contract – a smart contract deployed to the network, controlled by code.

Both account types have the ability to:

Receive, hold and send ETH and tokens
Interact with deployed smart contracts

So your Ethereum wallet is an interface for interacting with a keypair for a user-controlled account.

Also a smart contract is a type of account. When it is deployed, it has its own Ethereum address and is able to receive and hold ETH.

Addresses

Accounts have an address. Since we frequently use these addresses when writing smart contracts, Solidity has a data type called address. Let’s define a variable of type address named owner, and initialize owner with an Ethereum address.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    uint rate;
    address owner;
    
    constructor() {
        owner = msg.sender;
    }

    function getRate() public view returns (uint) {
         return rate;
    }   
    
    function setRate(uint _rate) public {
        rate = _rate;
    }
}

Constructors

You’ll notice we added a special function called the constructor. The constructor is called only once – when the contract is deployed. The constructor isn’t called again after that unless it is redeployed.

You’ll also notice we set the owner address equal to msg.sender. What is msg.sender? msg is a globally defined variable and the sender attribute contains the address that called the function. Since we set this value in the constructor, and the constructor is only called on deployment, the msg.sender will be the Ethereum address that deployed the contract. Since whoever deploys the contract owns the Calendar, we will be able to verify that any configuration functions are only being called by the contract owner.

Permissions and require()

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Calend3 {
    uint rate;
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }

    function getRate() public view returns (uint) {
         return rate;
    }   
    
    function setRate(uint _rate) public {
        require(msg.sender == owner, "Only the owner can set the rate");
        rate = _rate;
    }
}

Writing a Hardhat Test Script

So far we have only used hardhat compile and corrected compilation errors and syntax. But we haven’t really tested that our program logic is correct. Hardhat provides functionality for writing tests and logging output. Let’s take advantage of this.

Test setting the rate

You should have renamed the sample-script.js file in your test/ directory to test.js. Let’s open this file and make it look like this:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Calend3", function () {
  it("Set rate", async function () {
    const Contract = await ethers.getContractFactory("Calend3");
    const contract = await Contract.deploy();
    await contract.deployed();

    const tx = await contract.setRate(1000);

    // wait until the transaction is mined
    await tx.wait();

    // verify rate is set correctly
    expect(await contract.getRate()).to.equal(1000);
  });
});

Test permissions

Let’s add one more test to make sure our permissions are correct. We will use the ethers.getSigners() function to get a list of Ethereum account addresses from the hardhat development environment. We will then call setRate() with a different Ethereum address using the connect() function. We expect this to fail with the message ‘Only the owner can set the rate’.

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Calend3", function () {
  it("Set rate", async function () {
    const Contract = await ethers.getContractFactory("Calend3");
    const contract = await Contract.deploy();
    await contract.deployed();

    const tx = await contract.setRate(1000);

    // wait until the transaction is mined
    await tx.wait();

    // verify rate is set correctly
    expect(await contract.getRate()).to.equal(1000);

    // get addresses
    const [owner, addr1, addr2] = await ethers.getSigners();

    // call setRate using a different account address
    // this should fail since this address is not the owner
    await expect(
      contract.connect(addr1).setRate(500)
    ).to.be.revertedWith('Only the owner can set the rate');
  });
});