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:
$
touchtest
/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"
)
;
contract
(
"Greeter"
,
(
)
=>
{
it
(
"has been deployed successfully"
,
async
(
)
=>
{
const
greeter
=
await
GreeterContract
.
deployed
(
)
;
assert
(
greeter
,
"contract was not deployed"
)
;
}
)
;
}
)
;
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.Truffle tests use Mocha, but with a twist. The
contract
function will act similar to the built-indescribe
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.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.
Running our tests, we will receive an error that looks like this:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Migrations.solError: 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
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.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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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!
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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.solContract: 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.