Chapter 4. Our First Smart Contract

Now that we have everything installed, it is time to build our first contract. In following the tradition of introductory programming books, our first program will greet us with “Hello, World!”

In developing this program, we are going to learn how to use the tools provided by Truffle for creating and testing our application. We will also begin to explore the Solidity language, including a look at functions and state variables.

Our goal for this chapter is to find a rhythm in how we go about building our application and discovering if we are on the right track. To help us there, we will adopt test-driven development (TDD) for that instant feedback loop.

Let start by setting up our project.

Setup

As we get set up, we will need a directory to hold our new application. Let’s first create a directory called greeter and change into our new directory. Open your terminal and use the following commands:

$ mkdir greeter
$ cd greeter

We are now going to initialize a new Truffle project as follows:

$ truffle init

This command will generate the following output:

✔ Preparing to download
✔ Downloading
✔ Cleaning up temporary files
✔ Setting up box

Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test

Our greeter directory should now include the following files:

greeter
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
└── truffle-config.js

Notice that the commands specified in the output line up well with the directory structure generated when initializing our application. truffle compile will compile all the contracts in the contracts directory, truffle migrate will deploy our compiled contracts by running the scripts in our migrations directory, and lastly, truffle test will run the tests in our test directory.

The last item created for you is the truffle-config.js file. This is where we will place our application-specific configurations.

Now that we have our initial structure, we are ready to begin development.

Our First Test

As we implement features for our contracts, we will use TDD to take advantage of the short feedback loop it provides. If you are not familiar with TDD, it is a way of writing software where we first start with a failing test and then write the code required to make the test pass. Once everything is working, we can then refactor the code to make it more maintainable.

The test support Truffle provides is one of the areas where the toolbelt truly shines. It offers testing support in both JavaScript and Solidity; for our examples, we’ll use JavaScript since it is far more widely adopted, which makes it easier to find additional resources should you get stuck. If you would like to explore writing tests in Solidity, you can reference the Truffle testing documentation.

Our first test in Example 4-1 is going to make sure our empty contract can deploy properly. This may seem unnecessary, but the errors we will experience in getting this to pass provide a great way to see some of the errors we are likely to encounter in our career.

In the test directory, create a file called greeter_test.js:

$ touch test/greeter_test.js

Then add the test code, as in Example 4-1.

Example 4-1. Testing our contract can deploy
const GreeterContract = artifacts.require("Greeter"); 1

contract("Greeter", () => {                           2
  it("has been deployed successfully", async () => {  3
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract was not deployed");    4
  });
});
1

Truffle provides a way to load and interact with contracts that have been compiled through the artifacts.require function. Here, you will pass in the name of the contract, not the name of the file since a file may contain multiple contract declarations.

2

Truffle tests use Mocha, but with a twist. The contract function will act similar to the built-in describe but with the added benefit of using Truffle’s clean room feature. This feature means that fresh contracts will be deployed before the tests nested within are executed. This helps prevent state from being shared between different test groups.

3

Every interaction with the blockchain is going to be asynchronous, so instead of using Promises and the Promise.prototype.then method, we will take advantage of the async/await syntax now available in JavaScript.

4

If the greeter is truthy (exists), our test will pass.

Running our tests, we will receive an error that looks like this:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol

Error: Could not find artifacts for Greeter from any sources
    at Resolver.require (/usr/local/lib/node_modules/truffle/build/webpack:...
    at TestResolver.require (/usr/local/lib/node_modules/truffle/build/...
    at Object.require (/usr/local/lib/node_modules/truffle/build/webpack:...

    ...omitted..

Truffle v5.0.31 (core: 5.0.31)
Node v12.8.0

This gives us actionable feedback. The error tells us that after our contracts compiled, Truffle could not find one called Greeter. Since we have not created this contract yet, this error is perfectly reasonable. However, if you have already created a contract and are still getting this error, it is likely caused by a typo in the contract declaration found in the Solidity file or in the artifacts.require statement.

Let’s create the Greeter file and add the code from Example 4-2 to see what feedback running our test suite will provide.

In the terminal, create the Greeter file:

$ touch contracts/Greeter.sol
Example 4-2. Empty Greeter contract
pragma solidity >= 0.4.0 < 0.7.0; 1

contract Greeter { 2

}
1

The pragma line is a compiler instruction. Here we tell the Solidity compiler that our code is compatible with Solidity version 0.4.0 up to, but not including, version 0.7.0.

2

The contracts in Solidity are very similar to classes in object-oriented programming languages. The data and functions or methods defined within the contract’s opening and closing curly braces will be isolated to that contract.

With these changes made, let’s run our tests again:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    1) has been deployed successfully
    > No events were emitted


  0 passing (34ms)
  1 failing

  1) Contract: Greeter
       has been deployed successfully:
     Error: Greeter has not been deployed to detected network...
      at Object.checkNetworkArtifactMatch (/usr/local/lib/node_modules/...
      at Function.deployed (/usr/local/lib/node_modules/truffle/build/...
      at processTicksAndRejections (internal/process/task_queues.js:85:5)
      at Context.<anonymous> (test/greeter_test.js:5:21)

The error here indicates that our contract does not yet exist on the network; in other words, it has not yet been deployed. Every time we run the truffle test command, Truffle first compiles our contracts, and then deploys them to a test network. In order to deploy our contract, we need to turn to another tool provided by the Truffle toolbelt: migrations.

Migrations are scripts written in JavaScript that are used to automate the deployment of our contracts. The default Migrations contract found in contracts/Migrations.sol is the contract that is deployed by migrations/1_initial_migration.js and is currently the only contract that has made its way to the test network. In order to add our Greeter contract to the network, we will need to create a migration using the code found in Example 4-3.

First, we will need to create the file to hold our migrations code:

$ touch migrations/2_deploy_greeter.js

Then we can add the code in Example 4-3.

Example 4-3. Deploying the Greeter contract
const GreeterContract = artifacts.require("Greeter");

module.exports = function(deployer) {
  deployer.deploy(GreeterContract);
}

Our initial migration does not have much going on. We use the provided deployer object to deploy the Greeter contract. For now, this is all we’ll need to do in order to have our contract available on the local test network, but don’t worry: we will go deeper into migrations in the next chapter. With that in place, run the tests one more time:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully


  1 passing (27ms)

Success! This test lets us know we have everything set up correctly and we are ready to begin implementing features.

Saying Hello

This is normally the point where we would use a call to some variant of printf or println to output our greeting, but in Solidity we do not have access to standard out, or the file system, the network, or any other input/output (I/O). What we do have are functions.

Once deployed, our smart contract will be stored on the Ethereum network at a specific address. It will be dormant until a request comes in asking it to perform some work and the work our contract can do is defined by our functions. What we want is a function that can say “Hello, World!” and just like before, we will start with a test.

In test/greeter_test.js, let’s add the test from Example 4-4.

Example 4-4. Testing for Hello, World!
describe("greet()", () => {
  it("returns 'Hello, World!'", async () => {
    const greeter = await GreeterContract.deployed();
    const expected = "Hello, World!";
    const actual = await greeter.greet();

    assert.equal(actual, expected, "greeted with 'Hello, World!'");
  });
});

In this case, we set an expected value, and then retrieve the value from our contract and see if they are equal. We have to mark the test function as async since we are going to make a call to our local test blockchain to interact with this contract.

Running the test results in the following:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      1) returns 'Hello, World!'
    > No events were emitted


  1 passing (42ms)
  1 failing

  1) Contract: Greeter
       greet()
         returns 'Hello, World!':
     TypeError: greeter.greet is not a function
      at Context.<anonymous> (test/greeter_test.js:13:36)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

When we focus in on the error, we see that greeter.greet is not a function. It’s time to add a function to our contract. Update the Greeter contract with the function in Example 4-5.

Example 4-5. Adding the greet function to Greeter
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {

    function greet() external pure returns(string memory) {
        return "Hello, World!";
    }

}

Here we created a function with the identifier or name greet, which does not take any parameters. Following the identifier, we indicated that our function is an external function. This means that it is part of our contract’s interface and can be called from other contracts, or from transactions, but cannot be called from within the contract or at least not without an explicit reference to the object it is being called on. Our other options here are public, internal, and private.

public functions are also part of the interface, meaning they can be called from other contracts or transactions, but additionally they can be called internally. This means you can use an implicit receiver of the message when invoking the method inside of a method.

internal and private functions must use the implicit receiver or, in other words, cannot be called on an object or on this. The main difference between these two modifiers is that private functions are only visible within the contract in which they are defined, and not in derived contracts.

Functions that will not alter the state of the contract’s variables can be marked as either pure or view. pure functions do not read from the blockchain. Instead, they operate on the data passed in or, as in our case, data that did not need any input at all. view functions are allowed to read data from the blockchain, but again they are restricted in that they cannot write to the blockchain.

After our declaration that this function is pure, we identify what we expect our function to return. Solidity does allow multiple return values, but in our case we will only have one value being returned: the string type. We also indicate that this is a value that is not referencing anything located in our contract’s persisted storage by using the keyword memory.

The body of our function returns the string we are looking for, “Hello, World!” This should satisfy the requirements of our test. But don’t take our word for it—run the tests again to verify:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (51ms)


  2 passing (82ms)

With this test passing, let’s move on to making our contract a little more flexible by giving our users the ability to change the greeting.

Making Our Contract Dynamic

Now that our contract has returned a hardcoded value, let’s continue and make the greeting dynamic. In order to do this, we need to add another function that allows us to set the message that will be returned by our greet() function.

Earlier we mentioned the “clean room” feature when using the contract function in our tests. This feature will deploy new instances of our contracts for use within the callback function for that block of code. In the test we are about to write, we want to make sure our state changes stay isolated from the rest of the tests so that we do not find ourselves in a situation where the order of our tests will impact the success or failure of our test suite. In order to do this, we will create another contract block in our test/greeter-test.js file, as illustrated in Example 4-6.

Example 4-6. Testing the greeting can be made dynamic
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", () => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    });
  });
});

contract("Greeter: update greeting", () => {
  describe("setGreeting(string)", () => {
    it("sets greeting to passed in string", async () => {
      const greeter = await GreeterContract.deployed()
      const expected = "Hi there!";

      await greeter.setGreeting(expected);
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeting was not updated");
    });
  });
});

You’ll recognize the setup is much like our previous test. We set a variable to hold our expected return value, which is the string we will also pass to the setGreeting function. We then update the greeting and ask the greet for its return value. These are both asynchronous calls requiring us to use the await keyword. Lastly, we check the value from greet against our expected value.

When running the tests, we get the following output:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (48ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      1) sets greeting to passed in string
    > No events were emitted


  2 passing (111ms)
  1 failing

  1) Contract: Greeter: update greeting
       setGreeting(string)
         sets greeting to passed in string:
     TypeError: greeter.setGreeting is not a function
      at Context.<anonymous> (test/greeter_test.js:26:21)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Our tests indicate that the setGreeting function does not yet exist; let’s add this function to our contract. Back in our contracts/Greeter.sol file, after our greet function, add the function signature from Example 4-7.

Example 4-7. Adding setGreeting() to Greeter
function setGreeting(string calldata greeting) external {

}

Our setGreeting function is intended to update the state of our contract with a new greeting, which means we need to accept a parameter for this new value. This new value is expected to be a string, and will be referred by the identifier greeting. Just like our greet function, this function is intended to be called from external scripts or other contracts and will not be referred to internally.

Because this function is being called from the outside world, the data being passed in as a parameter is not part of the contract’s persisted storage, but is included as part of the calldata and must be labeled with the data location calldata. The calldata location is only needed when the function is declared as external and when the data type of the parameter is a reference type such as a mapping, struct, string, or array. Using value types like int or address do not require this label.

With the function declared, if we run our tests now, we will get the following output:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (45ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      1) sets greeting to passed in string
    > No events were emitted


  2 passing (215ms)
  1 failing

  1) Contract: Greeter: update greeting
       setGreeting(string)
         sets greeting to passed in string:

      greeting was not updated
      + expected - actual

      -Hello, World!
      +Hi there!

      at Context.<anonymous> (test/greeter_test.js:29:14)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Reviewing the test failure, we learn that the value returned by the greet function was not what was expected. Our function does not yet do anything and therefore the greeting never changed.

In order to update a variable in one function, and have that variable be available in another function, we’ll need to store data in the contract’s persisted storage by using a state variable, as demonstrated in Example 4-8.

In our contracts/Greeter.sol file, add the code in Example 4-8 to the Greeter contract.

Example 4-8. Adding state variable to Greeter contract
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
    string private _greeting;

    function greet() external pure returns(string memory) {
        return "Hello, World!";
    }

    function setGreeting(string calldata greeting) external {

    }

}

State variables will be available to all functions defined inside of a contract, similar to instance variables or member variables of other object-oriented languages. They are also where we will store data that will exist for the entire lifetime of our contract. Like functions, state variables can be declared with different levels of visibility modifiers, including public, internal, and private. In our previous example, we used the private modifier, which means this variable is accessible in our Greeter contract.

Note

All data on the blockchain is publicly visible from the outside world. State variable modifiers only restrict how the data can be interacted with from within the contract or other contracts.

When writing a function that updates a state variable, as our setGreeting is about to do, we cannot name the parameter the same as our state variable. To avoid these types of conflicts, it is common practice to prefix state variable or parameter names with an underscore (_) character.

Let’s update the setGreeting function to set the state variable, as shown in Example 4-9.

Example 4-9. Updating state variable
function setGreeting(string calldata greeting) external {
    _greeting = greeting;
}

Even with this change, our test would still fail. What we now want to do is to update the greet function to read from this state variable, but there are some things we need to consider before making this change. The first is that our function is currently marked as pure. We will need to update the function to be a view function since we are now going to access data stored on the blockchain. Once we switch to reading from the state variable in our greet function, we will no longer have our default greeting and the initial test will fail. To address these issues, we will give greeting a default of “Hello, World!” We will also change the function from pure to view and update the return value to use the value stored in greeting. Example 4-10 shows our contract with all these changes made.

Example 4-10. Reading from our state variable
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
    string private _greeting = "Hello, World!";

    function greet() external view returns(string memory) {
        return _greeting;
    }

    function setGreeting(string calldata greeting) external {
        _greeting = greeting;
    }

}

After running our tests, we’ll now see all three tests are passing!

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (57ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (116ms)


  3 passing (224ms)

Making the Greeter Ownable

As it stands now, anyone can change the message of our Greeter contract. This may be fine in some cases, but it could also lead to someone changing the message to something less than welcoming. To prevent this, we will now add the idea of ownership to the contract, and then restrict the ability to change the greeting to the owner.

In order to do this, we want to set the owner of the Greeter contract to the address that deployed the contract. This means we’ll need to store the address during initialization, and for that, we’ll need to write a constructor function. We will also need to access some information from the msg object. The msg object is globally available and includes the calldata, sender of the message, the signature of the function being called, and the value (how much wei was sent).

Our first test is going to assert that an owner exists by invoking an owner getter function. Since this is not depending on changing state on anything, we will put this test in the initial Greeter block of tests, as shown in Example 4-11.

Example 4-11. Testing an owner exists
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", () => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    })
  });

  describe("owner()", () => {
    it("returns the address of the owner", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();

      assert(owner, "the current owner");
    });
  });
})

Running this test will generate the following failure:

$ truffle test

Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (54ms)
    owner()
      1) returns the address of the owner
    > No events were emitted

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (274ms)


  3 passing (409ms)
  1 failing

  1) Contract: Greeter
       owner()
         returns the address of the owner:
     TypeError: greeter.owner is not a function
      at Context.<anonymous> (test/greeter_test.js:22:35)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

This failure looks familiar. We do not have a function defined on our Greeter contract called owner. Since this is a getter function, we’ll need to add a state variable that will hold the address of the owner, and then our function should return that address. The Solidity language provides two address types: one is address and the other is address payable. The difference between them is that address payable gives access to the transfer and send methods, and variables of this type can also receive ether. We are not sending ether to this address and we can use the address type for our purposes.

Let’s go ahead and update the Greeter contract with these changes, as illustrated in Example 4-12.

Example 4-12. Adding the ownership state variable and getter function
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
  string private _greeting = "Hello, World!";
  address private _owner;

  function greet() external view returns(string memory) {
    return _greeting;
  }

  function setGreeting(string calldata greeting) external {
    _greeting = greeting;
  }

  function owner() public view returns(address) {
    return _owner;
  }
}

Running our tests show that we are once again passing:

Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (57ms)
    owner()
      ✓ returns the address of the owner (50ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (125ms)


  4 passing (292ms)

What we really want to test here is that the owner address is the same as the deploying address. To do that we’ll now add a test to check that the ownership address is equal to the default account (the one used to deploy the contract). To do this, we’ll need access to the accounts in our test environment, and luckily Truffle has made this accessible to use via the accounts variable. We’ll need to pass this in to the contract level block of test code, as illustrated in Example 4-13.

Example 4-13. Test owner is the same as deployer
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", (accounts) => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    })
  });

  describe("owner()", () => {
    it("returns the address of the owner", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();

      assert(owner, "the current owner");
    });

    it("matches the address that originally deployed the contract", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();
      const expected = accounts[0];

      assert.equal(owner, expected, "matches address used to deploy contract");
    });
  });
})

Notice that our contract block is now passing in an accounts parameter to the function containing our test cases. Our new test is then asserting that the first account is the one that deployed the Greeter contract. When we run the tests, we will get a failure that informs us the owner and expected do not match. Now it’s time to write that constructor function.

Up until now we have been using the default constructor, constructor() public {}. Now we need to record the msg.sender as the owner when the contract is initialized. Our Greeter contract should now look like Example 4-14.

Example 4-14. Adding constructor to the Greeter contract
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
  string private _greeting = "Hello, World!";
  address private _owner;

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

  function greet() external view returns(string memory) {
    return _greeting;
  }

  function setGreeting(string calldata greeting) external {
    _greeting = greeting;
  }

  function owner() public view returns(address) {
    return _owner;
  }
}

With this change, our tests are once again passing.

Now that we know who created the contract, we can create a restriction that only the owner can update the greeting. This type of access control is normally done with a function modifier.

Function modifiers allow us to extend a function with code that can run before and/or after a function. These typically take the form of a guard clause and will prevent the function from being invoked if the clause is not met, which is exactly what we want for our Greeter contract.

Example 4-15 has updated the setGreeting tests, setting the expectation that only the owner can change the greeting.

Example 4-15. Test restricting setGreeting to owner
contract("Greeter: update greeting", (accounts) => {
  describe("setGreeting(string)", () => {
    describe("when message is sent by the owner", () => {
      it("sets greeting to passed in string", async () => {
        const greeter = await GreeterContract.deployed()
        const expected = "The owner changed the message";

        await greeter.setGreeting(expected);
        const actual = await greeter.greet();

        assert.equal(actual, expected, "greeting updated");
      });
    });

  describe("when message is sent by another account", () => {
    it("does not set the greeting", async () => {
      const greeter = await GreeterContract.deployed()
      const expected = await greeter.greet();

      try {
        await greeter.setGreeting("Not the owner", { from: accounts[1] });
      } catch(err) {
        const errorMessage = "Ownable: caller is not the owner"
        assert.equal(err.reason, errorMessage, "greeting should not update");
        return;
      }
      assert(false, "greeting should not update");
    });
  });
})

In our updated tests, we again pass in the accounts parameter to the contract callback function to make them available to our test cases. We also added a second section for the case when a non-owner account sends the message. We are now expecting an error to be raised when a non-owner address attempts to make this change. If you look at the setGreeting call, there is now a second parameter being passed in; it’s an object with a from property, and we are explicitly sending this message from a different account. This is also how we could set a value (in units of wei) to be sent to the contract as well.

Running our tests should result in a failure like this:

1 failing

1) Contract: Greeter: update greeting
     setGreeting(string)
       when message is sent by another account
         does not set the greeting:
   AssertionError: greeting should not update

Let’s create and apply our modifier to fix this right up. Back in our Greeter contract, after the constructor, add the following code:

modifier onlyOwner() {
  require(
    msg.sender == _owner,
    "Ownable: caller is not the owner"
  );
  _;
}

The modifier syntax looks very much like function syntax but without the visibility declaration. Here, our modifier is using the require function, where the first argument is an expression that will evaluate to a boolean. When this expression results in a false, the transaction is completely reverted, meaning all state changes are reversed and the program stops execution. The revert function also takes an optional string parameter that can be used to give more information to the caller as to why the operation failed.

The last part of our modifier function is the _; line. This line is where the function that is being modified will be called. If you put anything after this line, it will be run after the function body completes.

Now let’s update our setGreeter function to use the modifier by adding the onlyOwner declaration after external in the function definition:

function setGreeting(string calldata greeting) external onlyOwner {
  _greeting = greeting;
}

Running our tests, we see they are all passing!

This is great, but we are going to make one final change before we end this chapter. In Chapter 2, we discussed OpenZeppelin and how they have created contracts that can be used as the basis for creating tokens. Well, they also have contracts that implement the idea of ownership, and we are duplicating some of that behavior. Instead of duplicating it, we will update our Greeter contract to leverage their implementation.

Back in our terminal, in the root directory of our application, enter the following command:

$ npm install openzeppelin-solidity

Once that has completed, update the top of the Greeter contract to look like Example 4-16.

Example 4-16. Inheriting from Ownable
pragma solidity >= 0.4.0 < 0.7.0;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract Greeter is Ownable {
  ...rest of file...

Here, we added an import statement that will pull in all the global symbols from the imported file, such as Ownable, and make them available in the current scope. The next thing to notice is that our Greeter contract now inherits from Ownable through the is syntax. Solidity supports multiple inheritance much like Python or even C++. The inheriting classes are listed with a comma separating them.

With this in place, we can remove our implementations of the onlyOwner modifier, the owner getter function, and the constructor function since Ownable provides these definitions. This may seem like overkill, and normally I would agree with that thought. However, using code that has been through a thorough audit and well tested is prudent when working with smart contracts since security of smart contracts is critically important.

This wraps up our Greeter contract. But before we move on, let’s reflect on what we have learned in this chapter.

Summary

So far in our journey, we have learned how to create a new smart contract project using truffle init, which provided the directory structure for our application. This structure includes directories to house our contracts, tests, and migrations.

We wrote a deployment test that helped guide us through the initial migration needed to get our contract onto the test network. This allowed our subsequent tests to interact with the deployed contract.

Lastly, we started exploring the Solidity language and learned about the different function visibility modifiers (external, public, internal, private). Those same modifiers are also available for state variables (with the exception of external) used in persisting data on the blockchain. We also implemented the concept of Ownable and refactored to using the OpenZeppelin contract via inheritance.

In the next chapter, we are going to dive into how to deploy our contract locally and also onto one of the publicly available test networks.

Get Hands-On Smart Contract Development with Solidity and Ethereum now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.